--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -291683,16 +291683,76 @@
{}
]
],
"netinfo/META.yml": [
[
{}
]
],
+ "network-error-logging/META.yml": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/README.md": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/clear-policy-pass.png": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/clear-policy-pass.png.sub.headers": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/lock.py": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/nel.sub.js": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/no-policy-pass.png": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/pass.png": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/pass.png.sub.headers": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/report.py": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/subdomains-pass.png": [
+ [
+ {}
+ ]
+ ],
+ "network-error-logging/support/subdomains-pass.png.sub.headers": [
+ [
+ {}
+ ]
+ ],
"notifications/META.yml": [
[
{}
]
],
"notifications/common.js": [
[
{}
@@ -368846,16 +368906,64 @@
]
],
"netinfo/netinfo-basics.html": [
[
"/netinfo/netinfo-basics.html",
{}
]
],
+ "network-error-logging/no-report-on-failed-cors-preflight.https.html": [
+ [
+ "/network-error-logging/no-report-on-failed-cors-preflight.https.html",
+ {}
+ ]
+ ],
+ "network-error-logging/no-report-on-subdomain-404.https.html": [
+ [
+ "/network-error-logging/no-report-on-subdomain-404.https.html",
+ {}
+ ]
+ ],
+ "network-error-logging/no-report-on-subdomain-success.https.html": [
+ [
+ "/network-error-logging/no-report-on-subdomain-success.https.html",
+ {}
+ ]
+ ],
+ "network-error-logging/reports-are-not-observable.https.html": [
+ [
+ "/network-error-logging/reports-are-not-observable.https.html",
+ {}
+ ]
+ ],
+ "network-error-logging/sends-report-on-404.https.html": [
+ [
+ "/network-error-logging/sends-report-on-404.https.html",
+ {}
+ ]
+ ],
+ "network-error-logging/sends-report-on-subdomain-dns-failure.https.html": [
+ [
+ "/network-error-logging/sends-report-on-subdomain-dns-failure.https.html",
+ {}
+ ]
+ ],
+ "network-error-logging/sends-report-on-success-with-subdomain-policy.https.html": [
+ [
+ "/network-error-logging/sends-report-on-success-with-subdomain-policy.https.html",
+ {}
+ ]
+ ],
+ "network-error-logging/sends-report-on-success.https.html": [
+ [
+ "/network-error-logging/sends-report-on-success.https.html",
+ {}
+ ]
+ ],
"notifications/constructor-basic.html": [
[
"/notifications/constructor-basic.html",
{}
]
],
"notifications/constructor-invalid.html": [
[
@@ -610623,16 +610731,96 @@
"netinfo/idlharness.any.js": [
"4610508b69b1e30db574e04f2651b40cb8b646f2",
"testharness"
],
"netinfo/netinfo-basics.html": [
"7d08e12280a881508c862ebaaeaa40a099cf8e35",
"testharness"
],
+ "network-error-logging/META.yml": [
+ "bc063177d1d14febb1c4bdb86e70cfdce8ca0b5b",
+ "support"
+ ],
+ "network-error-logging/README.md": [
+ "7cf2c6fdceed95b3911deb69542a6820acda479d",
+ "support"
+ ],
+ "network-error-logging/no-report-on-failed-cors-preflight.https.html": [
+ "3a35651b4ef549a0510a83df923fd6b9d642b97c",
+ "testharness"
+ ],
+ "network-error-logging/no-report-on-subdomain-404.https.html": [
+ "462f99e842317cf36c4d7a76ad13af00b1692d15",
+ "testharness"
+ ],
+ "network-error-logging/no-report-on-subdomain-success.https.html": [
+ "5fd6d4fb41231c5ca5f345b890927c5d1b9411ab",
+ "testharness"
+ ],
+ "network-error-logging/reports-are-not-observable.https.html": [
+ "35ab4f3c23507617c4f26981339741d9b3c385be",
+ "testharness"
+ ],
+ "network-error-logging/sends-report-on-404.https.html": [
+ "38bdc014501e90f5a99bae1ac0d433191f557afb",
+ "testharness"
+ ],
+ "network-error-logging/sends-report-on-subdomain-dns-failure.https.html": [
+ "8913857af8acb01760589b6a7546a110a359f192",
+ "testharness"
+ ],
+ "network-error-logging/sends-report-on-success-with-subdomain-policy.https.html": [
+ "fce12cd3e96cf327222faea4bdaeba5835f4f4ce",
+ "testharness"
+ ],
+ "network-error-logging/sends-report-on-success.https.html": [
+ "68fddaa0c70b8dd0fd22194b351ba1157f836bdc",
+ "testharness"
+ ],
+ "network-error-logging/support/clear-policy-pass.png": [
+ "2fa1e0ac0663a65deae6602621521cc2844b93de",
+ "support"
+ ],
+ "network-error-logging/support/clear-policy-pass.png.sub.headers": [
+ "1085b8a987c56fd8b0412f4032f6957e58447ace",
+ "support"
+ ],
+ "network-error-logging/support/lock.py": [
+ "8c88250bde00b4a62cc99131fdaa09e97f716369",
+ "support"
+ ],
+ "network-error-logging/support/nel.sub.js": [
+ "c6b4783bd94c00579047627b6c2b137478ae1c2e",
+ "support"
+ ],
+ "network-error-logging/support/no-policy-pass.png": [
+ "2fa1e0ac0663a65deae6602621521cc2844b93de",
+ "support"
+ ],
+ "network-error-logging/support/pass.png": [
+ "2fa1e0ac0663a65deae6602621521cc2844b93de",
+ "support"
+ ],
+ "network-error-logging/support/pass.png.sub.headers": [
+ "70796e913ace97d4d1a21ac0b1c19f6fbb6d01fc",
+ "support"
+ ],
+ "network-error-logging/support/report.py": [
+ "7c05b51b9eb011f4d32bd5e774f6a0d3ead2cd9c",
+ "support"
+ ],
+ "network-error-logging/support/subdomains-pass.png": [
+ "2fa1e0ac0663a65deae6602621521cc2844b93de",
+ "support"
+ ],
+ "network-error-logging/support/subdomains-pass.png.sub.headers": [
+ "50124b8cfcdfd6efe91a0613ec34d3c8ca0a10dc",
+ "support"
+ ],
"notifications/META.yml": [
"2cb7972705c7b9ef00375dfa4258e92edb15fb21",
"support"
],
"notifications/body-basic-manual.html": [
"a479917aeef9f3b777d152148a3b75cb8df7f494",
"manual"
],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/META.yml
@@ -0,0 +1,2 @@
+suggested_reviewers:
+ - dcreager
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/README.md
@@ -0,0 +1,74 @@
+# Network Error Logging
+
+The tests in this directory exercise the user agent's implementation of [Network
+Error Logging](https://w3c.github.io/network-error-logging/) and
+[Reporting](https://w3c.github.io/reporting/).
+
+## Collector
+
+Each test case generates a unique `reportID` that is used to distinguish the NEL
+reports generated by that test case.
+
+The [support/report.py][] file is a [Python file handler][] that can be used as
+a Reporting collector. Its default operation is to save any reports that it
+receives into the [stash][]. If you pass in the optional `op` URL parameter,
+with a value of `retrieve_report`, it will instead return a list of all of the
+reports received for a particular `reportID`.
+
+[Python file handler]: https://wptserve.readthedocs.io/en/latest/handlers.html#python-file-handlers
+[stash]: https://wptserve.readthedocs.io/en/latest/stash.html
+[support/report.py]: support/report.py
+
+## Installing NEL policies
+
+NEL reports are only generated if the user agent has received a NEL policy for
+the origin of the request. The current request counts; if its response contains
+a policy, that policy is used for the current request and all future requests,
+modulo the policy's `max_age` field.
+
+Most of the test cases will therefore make a request or two to install NEL
+policies, and then make another request that should or should not be covered by
+those policies. It will then assert that NEL reports were or were not created,
+as required by the spec.
+
+The [support][] directory contains several images, each of which defines a
+particular "kind" of NEL policy (e.g., `include_subdomains` set vs unset, no
+policy at all, etc.). The [support/nel.sub.js][] file contains helper
+JavaScript methods for requesting those images, so that the test cases
+themselves are more descriptive.
+
+[support]: support
+[support/nel.sub.js]: support/nel.sub.js
+
+## Avoiding spurious reports
+
+NEL policies apply to **all** future requests to the origin. We therefore serve
+all of the test case's "infrastructure" (the test case itself,
+[support/report.py][] and [support/nel.sub.js][]) on a different origin than
+the requests that exercise the NEL implementation. That ensures that we don't
+have to wade through NEL reports about the infrastructure when verifying the NEL
+reports about the requests that we care about.
+
+## Browser configuration
+
+You must configure your browser's Reporting implementation to upload reports for
+a request immediately. The test cases do not currently have any timeouts; they
+assume that as soon as the Fetch API promise resolves, any NEL reports for the
+request have already been uploaded.
+
+## Test parallelism
+
+Because NEL policies are stored in a global cache in the user agent, we need to
+run the tests in this directory serially instead of in parallel. We implement a
+simple spin-lock in [support/lock.py][] to ensure that only one test is allowed
+to perform any NEL-related requests at a time.
+
+[support/lock.py]: support/lock.py
+
+## CORS preflights
+
+Reporting uploads are subject to CORS preflights. We want to test normal
+operation (when preflight requests succeed) as well as failures of the CORS
+preflight logic in the user agent. To support this, our test collector is
+configured to always reject the CORS preflight for a single domain (www2), and
+to always grant the CORS preflight for all other test subdomains.
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/no-report-on-failed-cors-preflight.https.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that NEL reports are not sent if the CORS preflight fails
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Make a request to a resource whose response headers include a NEL
+ // policy, but where the report uploader will reject the CORS preflight.
+ await fetchResourceWithBasicPolicy('www2');
+ // Because the CORS preflight is rejected, we should never receive a
+ // report about the request.
+ assert_false(await reportExists({
+ url: getURLForResourceWithBasicPolicy('www2'),
+ type: "network-error",
+ }));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/no-report-on-subdomain-404.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that include_subdomains policies do NOT report HTTP errors
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Make a request to a resource whose response headers include an
+ // include_subdomains NEL policy.
+ await fetchResourceWithIncludeSubdomainsPolicy();
+ // Make a request to another resource on a subdomain of the above. This
+ // resource doesn't exist, so the server should return a 404.
+ await fetchMissingResource('www');
+ // The include_subdomains policy that we just received should NOT cover
+ // the second request, since include_subdomains policies can only report
+ // on DNS errors.
+ assert_false(await reportExists({
+ url: getURLForMissingResource('www'),
+ type: "network-error",
+ }));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/no-report-on-subdomain-success.https.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that include_subdomains policies do NOT report successful requests
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Make a request to a resource whose response headers include an
+ // include_subdomains NEL policy.
+ await fetchResourceWithIncludeSubdomainsPolicy();
+ // Make a request to another resource on a subdomain of the above, which
+ // does not define its own NEL policy.
+ await fetchResourceWithNoPolicy('www');
+ // The include_subdomains policy that we just received should NOT cover
+ // the second request, since include_subdomains policies can only report
+ // on DNS errors.
+ assert_false(await reportExists({
+ url: getURLForResourceWithNoPolicy('www'),
+ type: "network-error",
+ }));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/reports-are-not-observable.https.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that NEL reports are not observable from JavaScript
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Register an observer for NEL reports.
+ var observer = new ReportingObserver((reports, _) => {
+ assert_unreached("NEL reports should not be observable");
+ }, {"types": ["network-error"]});
+ observer.observe();
+ // Make a request to a resource whose response headers include a NEL
+ // policy. If NEL reports are observable, this will queue a task that
+ // calls the observer function above (which we don't want).
+ await fetchResourceWithBasicPolicy();
+ // Wait for one second to give any observer callback task a chance to
+ // fire.
+ await new Promise(resolve => t.step_timeout(resolve, 1000 /* msec */));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-404.https.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that NEL reports are sent for HTTP errors
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Make a request to a resource whose response headers include a NEL
+ // policy.
+ await fetchResourceWithBasicPolicy();
+ // Make a request to another resource on the same domain. This resource
+ // doesn't exist, so the server should return a 404.
+ await fetchMissingResource();
+ // The 404 won't contain its own NEL policy, but the policy we received in
+ // the first request should cover the second request, too, since they're
+ // at the same origin, so the collector should have received a report
+ // about it.
+ assert_true(await reportExists({
+ url: getURLForMissingResource(),
+ user_agent: navigator.userAgent,
+ type: "network-error",
+ body: {
+ method: "GET",
+ sampling_fraction: 1.0,
+ status_code: 404,
+ phase: "application",
+ type: "http.error",
+ },
+ metadata: {
+ content_type: "application/reports+json",
+ },
+ }));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-subdomain-dns-failure.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that include_subdomains policies report DNS failures for subdomains
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Make a request to a resource whose response headers include an
+ // include_subdomains NEL policy.
+ await fetchResourceWithIncludeSubdomainsPolicy();
+ // Make a request to another resource on a nonexistent subdomain of the
+ // above. Since the subdomain doesn't exist, the request should fail with
+ // a DNS error.
+ await fetchResourceWithNoPolicy('nonexistent').then((response) => {
+ assert_unreached("Request to nonexistent domain should fail");
+ }, (err) => {
+ // Silence the error, since it's expected.
+ });
+ // The include_subdomains policy that we just received should cover the
+ // second request, since include_subdomains policies can report on DNS
+ // errors, so the collector should have received a report about it.
+ assert_true(await reportExists({
+ url: getURLForResourceWithNoPolicy('nonexistent'),
+ user_agent: navigator.userAgent,
+ type: "network-error",
+ body: {
+ method: "GET",
+ sampling_fraction: 1.0,
+ status_code: 0,
+ phase: "dns",
+ type: "dns.name_not_resolved",
+ },
+ metadata: {
+ content_type: "application/reports+json",
+ },
+ }));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-success-with-subdomain-policy.https.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that NEL reports are sent for successful requests
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Make a request to a resource whose response headers include an
+ // include_subdomains NEL policy.
+ await fetchResourceWithIncludeSubdomainsPolicy();
+ // That policy should apply to the request that delivered it. Even though
+ // the policy has include_subdomains set, it SHOULD generate a full,
+ // non-downgraded report about the request, since the request has the
+ // same origin as the policy. (I.e., since the origins are the same, the
+ // include_subdomains setting is irrelevant.)
+ assert_true(await reportExists({
+ url: getURLForResourceWithIncludeSubdomainsPolicy(),
+ user_agent: navigator.userAgent,
+ type: "network-error",
+ body: {
+ method: "GET",
+ sampling_fraction: 1.0,
+ status_code: 200,
+ phase: "application",
+ type: "ok",
+ },
+ metadata: {
+ content_type: "application/reports+json",
+ },
+ }));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/sends-report-on-success.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>
+ Test that NEL reports are sent for successful requests
+ </title>
+ <script src='/resources/testharness.js'></script>
+ <script src='/resources/testharnessreport.js'></script>
+ <script src='./support/nel.sub.js'></script>
+</head>
+<body>
+ <script>
+ nel_test(async t => {
+ // Make a request to a resource whose response headers include a NEL
+ // policy.
+ await fetchResourceWithBasicPolicy();
+ // That policy should apply to the request that delivered it, so the
+ // collector should have received a report about the request.
+ assert_true(await reportExists({
+ url: getURLForResourceWithBasicPolicy(),
+ user_agent: navigator.userAgent,
+ type: "network-error",
+ body: {
+ method: "GET",
+ sampling_fraction: 1.0,
+ status_code: 200,
+ phase: "application",
+ type: "ok",
+ },
+ metadata: {
+ content_type: "application/reports+json",
+ },
+ }));
+ });
+ </script>
+</body>
+</html>
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
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
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/clear-policy-pass.png.sub.headers
@@ -0,0 +1,6 @@
+Expires: Mon, 26 Jul 1997 05:00:00 GMT
+Cache-Control: no-store, no-cache, must-revalidate
+Cache-Control: post-check=0, pre-check=0, false
+Pragma: no-cache
+Report-To: { "group": "nel-group", "max_age": 0, "endpoints": [] }
+NEL: {"max_age": 0}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/lock.py
@@ -0,0 +1,38 @@
+_LOCK_KEY = "network-error-logging:lock"
+_TIMEOUT = 5 # seconds
+
+def wait_for_lock(request):
+ t0 = time.time()
+ while time.time() - t0 < _TIMEOUT:
+ time.sleep(0.5)
+ value = request.server.stash.take(key=_LOCK_KEY)
+ if value is None:
+ return True
+ return False
+
+def lock(request, report_id):
+ with request.server.stash.lock:
+ # Loop until the lock is free
+ if not wait_for_lock(request):
+ return (503, [], "Cannot obtain lock")
+ request.server.stash.put(key=_LOCK_KEY, value=report_id)
+ return "Obtained lock for %s" % report_id
+
+def unlock(request, report_id):
+ with request.server.stash.lock:
+ lock_holder = request.server.stash.take(key=_LOCK_KEY)
+ if lock_holder != request_id:
+ # Return the lock holder to the stash
+ request.server.stash.put(key=_LOCK_KEY, value=lock_holder)
+ return (503, [], "Cannot release lock held by %s" % lock_holder)
+ return "Released lock for %s" % report_id
+
+def main(request, response):
+ op = request.GET.first("op")
+ report_id = request.GET.first("reportID")
+ if op == "lock":
+ return lock(request, report_id)
+ elif op == "unlock":
+ return unlock(request, report_id)
+ else:
+ return (400, [], "Invalid op")
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/nel.sub.js
@@ -0,0 +1,169 @@
+const reportID = "{{$id:uuid()}}";
+
+/*
+ * NEL tests have to run serially, since the user agent maintains a global cache
+ * of Reporting and NEL policies, and we don't want the policies for multiple
+ * tests to interfere with each other. These functions (along with a Python
+ * handler in lock.py) implement a simple spin lock.
+ */
+
+function obtainNELLock() {
+ return fetch("/network-error-logging/support/lock.py?op=lock&reportID=" + reportID);
+}
+
+function releaseNELLock() {
+ return fetch("/network-error-logging/support/lock.py?op=unlock&reportID=" + reportID);
+}
+
+function nel_test(callback, name, properties) {
+ promise_test(async t => {
+ await obtainNELLock();
+ await clearReportingAndNELConfigurations();
+ await callback(t);
+ await releaseNELLock();
+ }, name, properties);
+}
+
+/*
+ * Helper functions for constructing domain names that contain NEL policies.
+ */
+function _monitoredDomain(subdomain) {
+ if (subdomain == "www") {
+ return "{{hosts[alt][www]}}"
+ } else if (subdomain == "www1") {
+ return "{{hosts[alt][www1]}}"
+ } else if (subdomain == "www2") {
+ return "{{hosts[alt][www2]}}"
+ } else if (subdomain == "nonexistent") {
+ return "{{hosts[alt][nonexistent]}}"
+ } else {
+ return "{{hosts[alt][]}}"
+ }
+}
+
+function _getNELResourceURL(subdomain, suffix) {
+ return "https://" + _monitoredDomain(subdomain) +
+ ":{{ports[https][0]}}/network-error-logging/support/" + suffix;
+}
+
+/*
+ * Fetches a resource whose headers define a basic NEL policy (i.e., with no
+ * include_subdomains flag). We ensure that we request the resource from a
+ * different origin than is used for the main test case HTML file or for report
+ * uploads. This minimizes the number of reports that are generated for this
+ * policy.
+ */
+
+function getURLForResourceWithBasicPolicy(subdomain) {
+ return _getNELResourceURL(subdomain, "pass.png?id="+reportID);
+}
+
+function fetchResourceWithBasicPolicy(subdomain) {
+ const url = getURLForResourceWithBasicPolicy(subdomain);
+ return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches a resource whose headers define an include_subdomains NEL policy.
+ */
+
+function getURLForResourceWithIncludeSubdomainsPolicy(subdomain) {
+ return _getNELResourceURL(subdomain, "subdomains-pass.png?id="+reportID);
+}
+
+function fetchResourceWithIncludeSubdomainsPolicy(subdomain) {
+ const url = getURLForResourceWithIncludeSubdomainsPolicy(subdomain);
+ return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches a resource whose headers do NOT define a NEL policy. This may or may
+ * not generate a NEL report, depending on whether you've already successfully
+ * requested a resource from the same origin that included a NEL policy.
+ */
+
+function getURLForResourceWithNoPolicy(subdomain) {
+ return _getNELResourceURL(subdomain, "no-policy-pass.png");
+}
+
+function fetchResourceWithNoPolicy(subdomain) {
+ const url = getURLForResourceWithNoPolicy(subdomain);
+ return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches a resource that doesn't exist. This may or may not generate a NEL
+ * report, depending on whether you've already successfully requested a resource
+ * from the same origin that included a NEL policy.
+ */
+
+function getURLForMissingResource(subdomain) {
+ return _getNELResourceURL(subdomain, "nonexistent.png");
+}
+
+function fetchMissingResource(subdomain) {
+ const url = getURLForMissingResource(subdomain);
+ return fetch(url, {mode: "no-cors"});
+}
+
+/*
+ * Fetches resources that clear out any existing Reporting or NEL configurations
+ * for all origins that any test case might use.
+ */
+
+function getURLForClearingConfiguration(subdomain) {
+ return _getNELResourceURL(subdomain, "clear-pass.png?id="+reportID);
+}
+
+async function clearReportingAndNELConfigurations(subdomain) {
+ await Promise.all([
+ fetch(getURLForClearingConfiguration(""), {mode: "no-cors"}),
+ fetch(getURLForClearingConfiguration("www"), {mode: "no-cors"}),
+ fetch(getURLForClearingConfiguration("www1"), {mode: "no-cors"}),
+ fetch(getURLForClearingConfiguration("www2"), {mode: "no-cors"}),
+ ]);
+ return;
+}
+
+/*
+ * Returns whether all of the fields in obj1 also exist in obj2 with the same
+ * values. (Put another way, returns whether obj1 and obj2 are equal, ignoring
+ * any extra fields in obj2.)
+ */
+
+function _isSubsetOf(obj1, obj2) {
+ for (const prop in obj1) {
+ if (typeof obj1[prop] === 'object') {
+ if (typeof obj2[prop] !== 'object') {
+ return false;
+ }
+ if (!_isSubsetOf(obj1[prop], obj2[prop])) {
+ return false;
+ }
+ } else if (obj1[prop] != obj2[prop]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/*
+ * Verifies that a report was uploaded that contains all of the fields in
+ * expected.
+ */
+
+async function reportExists(expected) {
+ var timeout =
+ document.querySelector("meta[name=timeout][content=long]") ? 50 : 1;
+ var reportLocation =
+ "/network-error-logging/support/report.py?op=retrieve_report&timeout=" +
+ timeout + "&reportID=" + reportID;
+ const response = await fetch(reportLocation);
+ const json = await response.json();
+ for (const report of json) {
+ if (_isSubsetOf(expected, report)) {
+ return true;
+ }
+ }
+ return false;
+}
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
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
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
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
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/pass.png.sub.headers
@@ -0,0 +1,6 @@
+Expires: Mon, 26 Jul 1997 05:00:00 GMT
+Cache-Control: no-store, no-cache, must-revalidate
+Cache-Control: post-check=0, pre-check=0, false
+Pragma: no-cache
+Report-To: { "group": "nel-group", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/network-error-logging/support/report.py?op=put&reportID={{GET[id]}}" }] }
+NEL: {"report_to": "nel-group", "max_age": 10886400, "success_fraction": 1.0}
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/report.py
@@ -0,0 +1,52 @@
+import time
+import json
+import re
+
+def retrieve_from_stash(request, key, timeout, default_value):
+ t0 = time.time()
+ while time.time() - t0 < timeout:
+ time.sleep(0.5)
+ value = request.server.stash.take(key=key)
+ if value is not None:
+ return json.dumps(value)
+
+ return default_value
+
+def main(request, response):
+ # Handle CORS preflight requests
+ if request.method == 'OPTIONS':
+ # Always reject preflights for one subdomain
+ if "www2" in request.headers["Origin"]:
+ return (400, [], "CORS preflight rejected for www2")
+ return [
+ ("Content-Type", "text/plain"),
+ ("Access-Control-Allow-Origin", "*"),
+ ("Access-Control-Allow-Methods", "post"),
+ ("Access-Control-Allow-Headers", "Content-Type"),
+ ], "CORS allowed"
+
+ op = request.GET.first("op");
+ key = request.GET.first("reportID")
+
+ if op == "retrieve_report":
+ try:
+ timeout = float(request.GET.first("timeout"))
+ except:
+ timeout = 0.5
+ return [("Content-Type", "application/json")], retrieve_from_stash(request, key, timeout, '[]')
+
+ # append new reports
+ new_reports = json.loads(request.body)
+ for report in new_reports:
+ report["metadata"] = {
+ "content_type": request.headers["Content-Type"],
+ }
+ with request.server.stash.lock:
+ reports = request.server.stash.take(key=key)
+ if reports is None:
+ reports = []
+ reports.extend(new_reports)
+ request.server.stash.put(key=key, value=reports)
+
+ # return acknowledgement report
+ return [("Content-Type", "text/plain")], "Recorded report"
new file mode 100644
index 0000000000000000000000000000000000000000..2fa1e0ac0663a65deae6602621521cc2844b93de
GIT binary patch
literal 1689
zc${^WX*3&n7smf;t))z9sgk5hM+alnTMSWZso0lN)UJe5)ig;FstDDQvBcI2qNSm=
zjeV*WYKe6;5{-sx^pH_gOD457Z_qdM{hbfbea`uvd+wKe?sI?faC1<QRhI<-C^#bQ
zyhQ#?Bx{*NqAGTwL0)8sZz25A0LUr-BQZd#P!mCEjH9c)G)q=q#_$y9dqJ}ZX=3dC
zFt+h=agjGM!1i`z7$!0TmT(;t4fBqUggH36dhjB2ZUP{s=xBG*J8^EE6zfvttCqIF
zl%qAvYS}b+!ye?!uhv_u+UN`-{HCC1Y$a8Ky3#f?a@1?eNsoUSRea#7?a^bW+)k6r
z$&r6l?Tklg9xti(D3Kj?Rch&6PpzPYQ9AacBY#Y;3W<O7GcUpIzue@$-B@RQ6%ard
zGofNR4j%A~#qU(|A16xCPU}K0@@4)7*&G{=!m1CZ4ze#c1;00C)~$KY)W;B@$4#km
zZMU+ToVy?dt4oWT$1L(V7BNdRWDR=flUSYJ!jEPB(?PWs#ueEewv{{!pZ)sbH^VP?
zUXo@=T?Z!FpAG`1E;LvLkdDRMq&LD=uasV@GqMn_WIOa5;~g7Uy!7F{Q}^@JN)6s{
zpzT$Yp#lB^<H_`Hzt7K!XUE|siQ{W;wGOmuus~)@MrHD-KQ%NgJnRiqqhVQtOrUVa
z_xmy7U%K}wsm=+61flz`bXxJ0%5{84edoA#Y!KW0=!ydFdy?`ABDK_?t!o5B*$zn^
zwOUOrHJtbj=gFE`Flp388nYo9icsGwCA!inceN$urPqP_+sn?>*@4kWRO}2ZGdWBA
zb+;A0Nr0VP@sE1hj(atiyDkNU^*fpmECINC`fg7kXLN+fzoQ=#)ceQ16JY*EcNyl?
z*nrc>a`^MQ*J()DE3NV&PmbrAcD>_`u$dCeaoav<%wKV0z_)94TvE><5Y1nsX|wO3
zPx_#DxYda<^T|p162T#fxxSw$3SQ!BPZhk1Q0*>QveTi+{KC&+Yh(6&<-59S2aIpY
z?%R2rgwyW3UP)&`u}Se~1so{&>Lf8Ou@qF7{H$E!qA~s%xt|=U;;wKli<h`ygoB^g
zFTtRX(e;9<$&$cuf!edbX9CO^6<)TNn@S4?h6a9~UUq5RbAhSSE~gE0HS#QOe0ieG
z9AF|m_FC9x(Z5a)Frz~c&YSb{;o@q*dQ{ou8#`tDeM4Eb3qzkMH%R-Ov^GX-=HPtk
zq8+{H@vCB6hO8gHr3k~L>Xv>8@h&t&y18Jlo(Eg<yMLnvPz-c<N6<=XWi_#orXSiO
zoHyQeKMhr5oWSJ#NUQ9!ct{ScPw{jltLIJH^N~XV!`vV@lC}=5$NEak^OWh863Gd<
zbG?6nwCOL?&mRsOd?mJ@R&+Yzl%j)_!?!t<Vf9wWr}d7|?fEK0RVlY_qXo#NxXDNq
z>pe^3j5Ns18o*)qS7<immW@~DK#h4MNpo9$O5X29TPP`1F#l(Gx><(mcON0vbqojk
z#!KodVx8ztBrJ~X6Kn51?W6oox#APR^kA}Q&2T1`aOz6A7AQy<9dyggb$xixP?0Y$
zmX_;ldYq*?n|M_9KB2(S<xzP7gt%cu69@94N~#1`oLu&&MPtrP)qggc`%bQF#$8i}
z1ZlW#BW~XKy^VYBiwVjTg<{csii!w9RGmZf;x{_nf+cgQPu92cwvd5zSgRk5+!3e}
ze|TbHe727~^>%yL3Lrj^JLcVGm-=EW-D>JGYYWW&vSI#N_i!Yj>?hMF`rY!t%jWXV
zL6>jTl;>PD=hr0dmid$Ba8+L7=8$k?vpWT>Brc$u1f;AI(_+)N_!#gJE#M#Ui|>>T
zR1~UKIV?)AE1oeB^{--uYc=iAZKy%x+Y^{5Zq%JiJZ=AX<-eV*^A7Oote?YSsnyPm
znT1-2hhmIWC(a;yjE~+d`s#GfzAW`-YDDXpbGl_Wo8{ipkdT0ir^iGw{DBe%zJhvp
zZDREk{n0`(LjtH*-mjYAK{jf)N{F*v^JLMvDo0>PW5R=Wt|D3H0G=e|9yAES>+1GG
ze9_r;3gi0ijb-g`h46IrT#+a@yebW^>$VzExM7amH3aa3_Ff1nb(fM%%FyLKlL@wa
z?C?0hK{0<~sqKY}Ioi)+5BYa)wX^4kt9N_(DMD)@TkfARkbbX!i`JU|hq>rw!}q$g
Vg^jxe1<?-yj`nVL%{HMK{{w#RF>?R_
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/network-error-logging/support/subdomains-pass.png.sub.headers
@@ -0,0 +1,6 @@
+Expires: Mon, 26 Jul 1997 05:00:00 GMT
+Cache-Control: no-store, no-cache, must-revalidate
+Cache-Control: post-check=0, pre-check=0, false
+Pragma: no-cache
+Report-To: { "group": "nel-group", "max_age": 10886400, "include_subdomains": true, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/network-error-logging/support/report.py?op=put&reportID={{GET[id]}}" }] }
+NEL: {"report_to": "nel-group", "max_age": 10886400, "include_subdomains": true, "success_fraction": 1.0}