nel.sub.js (9836B)
1 const reportID = "{{$id:uuid()}}"; 2 3 /* 4 * NEL tests have to run serially, since the user agent maintains a global cache 5 * of Reporting and NEL policies, and we don't want the policies for multiple 6 * tests to interfere with each other. These functions (along with a Python 7 * handler in lock.py) implement a simple spin lock. 8 */ 9 10 function obtainNELLock() { 11 return fetch("/network-error-logging/support/lock.py?op=lock&reportID=" + reportID); 12 } 13 14 function releaseNELLock() { 15 return fetch("/network-error-logging/support/lock.py?op=unlock&reportID=" + reportID); 16 } 17 18 function nel_test(callback, name, properties) { 19 promise_test(async t => { 20 await obtainNELLock(); 21 await assertNELIsImplemented(); 22 await clearReportingAndNELConfigurations(); 23 await callback(t); 24 await releaseNELLock(); 25 }, name, properties); 26 } 27 28 function nel_iframe_test(callback, name, properties) { 29 promise_test(async t => { 30 await obtainNELLock(); 31 await assertNELIsImplemented(); 32 await clearReportingAndNELConfigurationsInIframe(); 33 await callback(t); 34 await releaseNELLock(); 35 }, name, properties); 36 } 37 38 /* 39 * Helper functions for constructing domain names that contain NEL policies. 40 */ 41 function _monitoredDomain(subdomain) { 42 if (subdomain == "www") { 43 return "{{hosts[alt][www]}}" 44 } else if (subdomain == "www1") { 45 return "{{hosts[alt][www1]}}" 46 } else if (subdomain == "www2") { 47 return "{{hosts[alt][www2]}}" 48 } else if (subdomain == "nonexistent") { 49 return "{{hosts[alt][nonexistent]}}" 50 } else { 51 return "{{hosts[alt][]}}" 52 } 53 } 54 55 function _getNELResourceURL(subdomain, suffix, options = {}) { 56 return `https://${_monitoredDomain(subdomain)}:{{ports[https][0]}}/` + 57 (options.sanitize ? "" : `network-error-logging/support/${suffix}`); 58 } 59 60 /* 61 * Fetches a resource whose headers define a basic NEL policy (i.e., with no 62 * include_subdomains flag). We ensure that we request the resource from a 63 * different origin than is used for the main test case HTML file or for report 64 * uploads. This minimizes the number of reports that are generated for this 65 * policy. 66 */ 67 68 function getURLForResourceWithBasicPolicy(subdomain) { 69 return _getNELResourceURL(subdomain, "pass.png?id="+reportID+"&success_fraction=1.0"); 70 } 71 72 function getURLForResourceWithBasicPolicyv1(subdomain) { 73 return _getNELResourceURL(subdomain, "pass2.png?id="+reportID+"&success_fraction=1.0"); 74 } 75 76 function getSanitizedURLForResourceWithNoPolicy(subdomain) { 77 return _getNELResourceURL(subdomain, "no-policy-pass.png", { sanitize: true }); 78 } 79 80 function fetchResourceWithBasicPolicy(subdomain) { 81 const url = getURLForResourceWithBasicPolicy(subdomain); 82 return fetch(url, {mode: "no-cors"}); 83 } 84 85 function fetchResourceWithBasicPolicyv1(subdomain) { 86 const url = getURLForResourceWithBasicPolicyv1(subdomain); 87 return fetch(url, {mode: "no-cors"}); 88 } 89 90 function fetchResourceWithZeroSuccessFractionPolicy(subdomain) { 91 const url = _getNELResourceURL(subdomain, "pass.png?id="+reportID+"&success_fraction=0.0"); 92 return fetch(url, {mode: "no-cors"}); 93 } 94 95 /* 96 * Similar to the above methods, but fetch resources in an iframe. Allows matching 97 * full context of reports sent from an iframe that's same-site relative to the domains 98 * a policy set. 99 */ 100 101 function loadResourceWithBasicPolicyInIframe(subdomain) { 102 return loadResourceWithPolicyInIframe( 103 getURLForResourceWithBasicPolicy(subdomain)); 104 } 105 106 function loadResourceWithZeroSuccessFractionPolicyInIframe(subdomain) { 107 return loadResourceWithPolicyInIframe( 108 _getNELResourceURL(subdomain, "pass.png?id="+reportID+"&success_fraction=0.0")); 109 } 110 111 function clearResourceWithBasicPolicyInIframe(subdomain) { 112 return loadResourceWithPolicyInIframe( 113 getURLForClearingConfiguration(subdomain)); 114 } 115 116 function loadResourceWithPolicyInIframe(url) { 117 return new Promise((resolve, reject) => { 118 const frame = document.createElement('iframe'); 119 frame.src = url; 120 frame.onload = () => resolve(frame); 121 frame.onerror = () => reject('failed to load ' + url); 122 document.body.appendChild(frame); 123 }); 124 } 125 126 /* 127 * Fetches a resource whose headers define an include_subdomains NEL policy. 128 */ 129 130 function getURLForResourceWithIncludeSubdomainsPolicy(subdomain) { 131 return _getNELResourceURL(subdomain, "subdomains-pass.png?id="+reportID); 132 } 133 134 function fetchResourceWithIncludeSubdomainsPolicy(subdomain) { 135 const url = getURLForResourceWithIncludeSubdomainsPolicy(subdomain); 136 return fetch(url, {mode: "no-cors"}); 137 } 138 139 /* 140 * Fetches a resource whose headers do NOT define a NEL policy. This may or may 141 * not generate a NEL report, depending on whether you've already successfully 142 * requested a resource from the same origin that included a NEL policy. 143 */ 144 145 function getURLForResourceWithNoPolicy(subdomain) { 146 return _getNELResourceURL(subdomain, "no-policy-pass.png"); 147 } 148 149 function fetchResourceWithNoPolicy(subdomain) { 150 const url = getURLForResourceWithNoPolicy(subdomain); 151 return fetch(url, {mode: "no-cors"}); 152 } 153 154 /* 155 * Fetches a resource that doesn't exist. This may or may not generate a NEL 156 * report, depending on whether you've already successfully requested a resource 157 * from the same origin that included a NEL policy. 158 */ 159 160 function getURLForMissingResource(subdomain) { 161 return _getNELResourceURL(subdomain, "nonexistent.png"); 162 } 163 164 function fetchMissingResource(subdomain) { 165 const url = getURLForMissingResource(subdomain); 166 return fetch(url, {mode: "no-cors"}); 167 } 168 169 /* 170 * Fetches a resource that can be cached without validation. 171 */ 172 173 function getURLForCachedResource(subdomain) { 174 return _getNELResourceURL(subdomain, "cached-for-one-minute.png"); 175 } 176 177 function fetchCachedResource(subdomain) { 178 const url = getURLForCachedResource(subdomain); 179 return fetch(url, {mode: "no-cors"}); 180 } 181 182 /* 183 * Fetches a resource that can be cached but requires validation. 184 */ 185 186 function getURLForValidatedCachedResource(subdomain) { 187 return _getNELResourceURL(subdomain, "cached-with-validation.py"); 188 } 189 190 function fetchValidatedCachedResource(subdomain) { 191 const url = getURLForValidatedCachedResource(subdomain); 192 return fetch(url, {mode: "no-cors"}); 193 } 194 195 /* 196 * Fetches a resource that redirects once before returning a successful 197 * response. 198 */ 199 200 function getURLForRedirectedResource(subdomain) { 201 return _getNELResourceURL(subdomain, "redirect.py?id="+reportID); 202 } 203 204 function fetchRedirectedResource(subdomain) { 205 const url = getURLForRedirectedResource(subdomain); 206 return fetch(url, {mode: "no-cors"}); 207 } 208 209 /* 210 * Fetches resources that clear out any existing Reporting or NEL configurations 211 * for all origins that any test case might use. 212 */ 213 214 function getURLForClearingConfiguration(subdomain) { 215 return _getNELResourceURL(subdomain, "clear-policy-pass.png?id="+reportID); 216 } 217 218 async function clearReportingAndNELConfigurations(subdomain) { 219 await Promise.all([ 220 fetch(getURLForClearingConfiguration(""), {mode: "no-cors"}), 221 fetch(getURLForClearingConfiguration("www"), {mode: "no-cors"}), 222 fetch(getURLForClearingConfiguration("www1"), {mode: "no-cors"}), 223 fetch(getURLForClearingConfiguration("www2"), {mode: "no-cors"}), 224 ]); 225 return; 226 } 227 228 async function clearReportingAndNELConfigurationsInIframe(subdomain) { 229 await Promise.all([ 230 clearResourceWithBasicPolicyInIframe(""), 231 clearResourceWithBasicPolicyInIframe("www"), 232 clearResourceWithBasicPolicyInIframe("www1"), 233 clearResourceWithBasicPolicyInIframe("www2"), 234 ]); 235 return; 236 } 237 238 /* 239 * Returns whether all of the fields in obj1 also exist in obj2 with the same 240 * values. (Put another way, returns whether obj1 and obj2 are equal, ignoring 241 * any extra fields in obj2.) 242 */ 243 244 function _isSubsetOf(obj1, obj2) { 245 for (const prop in obj1) { 246 if (typeof obj1[prop] === 'object') { 247 if (typeof obj2[prop] !== 'object') { 248 return false; 249 } 250 if (!_isSubsetOf(obj1[prop], obj2[prop])) { 251 return false; 252 } 253 } else if (obj1[prop] != obj2[prop]) { 254 return false; 255 } 256 } 257 return true; 258 } 259 260 /* 261 * Verifies that a report was uploaded that contains all of the fields in 262 * expected. 263 */ 264 265 async function reportExists(expected, retain_reports, timeout) { 266 if (!timeout) { 267 timeout = document.querySelector("meta[name=timeout][content=long]") ? 50 : 1; 268 } 269 var reportLocation = 270 "/reporting/resources/report.py?op=retrieve_report&timeout=" + 271 timeout + "&reportID=" + reportID; 272 if (retain_reports) 273 reportLocation += "&retain=1"; 274 const response = await fetch(reportLocation); 275 const json = await response.json(); 276 for (const report of json) { 277 if (_isSubsetOf(expected, report)) { 278 return true; 279 } 280 } 281 return false; 282 } 283 284 /* 285 * Verifies that reports were uploaded that contains all of the fields in 286 * expected. 287 */ 288 289 async function reportsExist(expected_reports, retain_reports) { 290 const timeout = 10; 291 let reportLocation = 292 "/reporting/resources/report.py?op=retrieve_report&timeout=" + 293 timeout + "&reportID=" + reportID; 294 if (retain_reports) 295 reportLocation += "&retain"; 296 // There must be the report of pass.png, so adding 1. 297 const min_count = expected_reports.length + 1; 298 reportLocation += "&min_count=" + min_count; 299 const response = await fetch(reportLocation); 300 const json = await response.json(); 301 for (const expected of expected_reports) { 302 const found = json.some((report) => { 303 return _isSubsetOf(expected, report); 304 }); 305 if (!found) 306 return false; 307 } 308 return true; 309 } 310 311 // this runs first to avoid testing on browsers not implementing NEL 312 async function assertNELIsImplemented() { 313 await fetchResourceWithBasicPolicy(); 314 // Assert that the report was generated 315 assert_implements(await reportExists({ 316 url: getURLForResourceWithBasicPolicy(), 317 type: "network-error" 318 }, false, 1), "'Basic NEL support: missing network-error report'"); 319 }