diff options
Diffstat (limited to 'testing/web-platform/tests/reporting')
51 files changed, 1359 insertions, 0 deletions
diff --git a/testing/web-platform/tests/reporting/META.yml b/testing/web-platform/tests/reporting/META.yml new file mode 100644 index 0000000000..70f3136dd3 --- /dev/null +++ b/testing/web-platform/tests/reporting/META.yml @@ -0,0 +1,5 @@ +spec: https://w3c.github.io/reporting/ +suggested_reviewers: + - clelland + - dcreager + - igrigorik diff --git a/testing/web-platform/tests/reporting/README.md b/testing/web-platform/tests/reporting/README.md new file mode 100644 index 0000000000..52f08265c5 --- /dev/null +++ b/testing/web-platform/tests/reporting/README.md @@ -0,0 +1,19 @@ +Tests for the [Reporting API](https://w3c.github.io/reporting/). + +The tests in this directory validate the generic functionaity of the Reporting +API. Since reports are not actually generated by that specification, these tests +occasionally make use of other integrations, like CSP or Permissions Policy. + +## Testing integration with the Reporting API + +More comprehensive tests for other specifications' generated reports should be +in those specs' respective directories. + +There are two general methods of testing reporting integration: + +* The simpler is with the ReportingObserver interface, generating reports + within a document and reading them from script running in that document. +* For reports which cannot be observed from a document, there is a reporting + collector provided which can receive reports sent over HTTP and then serve + them in response to queries from the test script. See resources/README.md for + details. diff --git a/testing/web-platform/tests/reporting/bufferSize.html b/testing/web-platform/tests/reporting/bufferSize.html new file mode 100644 index 0000000000..0e50526772 --- /dev/null +++ b/testing/web-platform/tests/reporting/bufferSize.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Reporting: Buffer size</title> +<link rel="author" title="Paul Meyer" href="paulmeyer@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> + // Test the buffer size (100) of ReportingObserver. + promise_test(async function(test) { + for (let i = 0; i != 110; ++i) + await test_driver.generate_test_report("" + i); + + let reports = await new Promise(resolve => { + let observer = new ReportingObserver(resolve, {buffered:true}); + observer.observe(); + }); + + // Only (the most recent) 100 reports should be observed, even though + // 110 were buffered. + assert_equals(reports.length, 100); + for (let i = 0; i != 100; ++i) { + assert_equals(reports[i].body.message, "" + (i + 10)); + } + }, "Buffer size"); +</script> diff --git a/testing/web-platform/tests/reporting/cross-origin-report-no-credentials.https.sub.html b/testing/web-platform/tests/reporting/cross-origin-report-no-credentials.https.sub.html new file mode 100644 index 0000000000..f10d4cef3e --- /dev/null +++ b/testing/web-platform/tests/reporting/cross-origin-report-no-credentials.https.sub.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that reports are sent without credentials to cross-origin endpoints</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> +<body> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = 'fe5ca189-269a-4e74-a4dd-d7a3b33139d5'; + + promise_test(async t => { + // Set credentials, and set up test to clear them afterwards. + await fetch('/cookies/resources/set-cookie.py?name=report&path=%2F', {mode: 'no-cors', credentials: 'include', cache: 'no-store'}); + t.add_cleanup(() => fetch("/cookies/resources/set.py?report=; path=%2F; expires=Thu, 01 Jan 1970 00:00:01 GMT")); + + // Trigger a CSP error. + await new Promise(resolve => { + const img = document.createElement('img'); + img.src = "/reporting/resources/fail.png"; + img.addEventListener('error', resolve); + document.body.appendChild(img); + }); + + // Wait for report to be received. + const reports = await pollReports(endpoint, id); + checkReportExists(reports, 'csp-violation', location.href); + + // Validate that credentials were not sent to cross-origin endpoint. + const cookies = await pollCookies(endpoint, id); + assert_equals(Object.keys(cookies).length, 0, "Credentials were absent from report"); + }, "Reporting endpoints did not receive credentials."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/cross-origin-report-no-credentials.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/cross-origin-report-no-credentials.https.sub.html.sub.headers new file mode 100644 index 0000000000..24eaf19fec --- /dev/null +++ b/testing/web-platform/tests/reporting/cross-origin-report-no-credentials.https.sub.html.sub.headers @@ -0,0 +1,2 @@ +Reporting-Endpoints: csp-endpoint="https://{{domains[www1]}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=fe5ca189-269a-4e74-a4dd-d7a3b33139d5" +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-endpoint diff --git a/testing/web-platform/tests/reporting/cross-origin-reports-isolated.https.sub.html b/testing/web-platform/tests/reporting/cross-origin-reports-isolated.https.sub.html new file mode 100644 index 0000000000..4e9cb1eb01 --- /dev/null +++ b/testing/web-platform/tests/reporting/cross-origin-reports-isolated.https.sub.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that reports from different origins are not sent together</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> +<body> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = 'd0d517bf-891b-457a-b970-8b2b2c81a0bf'; + + promise_test(async t => { + + // Attach a cross-origin iframe which should post back here immediately + // before generating a CSP error. That error should be reported to the + // same endpoint that this frame reports to. + await new Promise(resolve => { + const iframe = document.createElement('iframe'); + iframe.src = "https://{{domains[www]}}:{{ports[https][0]}}/reporting/resources/csp-error.https.sub.html"; + addEventListener('message', resolve); + document.body.appendChild(iframe); + }); + + // Trigger a CSP error and report in this frame as well. + await new Promise(resolve => { + const img = document.createElement('img'); + img.src = "/reporting/resources/fail.png"; + img.addEventListener('error', resolve); + document.body.appendChild(img); + }); + + // Wait for 2 reports to be received. + const reports = await pollReports(endpoint, id, 2); + assert_equals(reports.length, 2); + + // Validate that reports were sent in separate requests. + const request_count = await pollNumResults(endpoint, id); + assert_equals(request_count, 2); + }, "Reports were sent in two requests."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/cross-origin-reports-isolated.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/cross-origin-reports-isolated.https.sub.html.sub.headers new file mode 100644 index 0000000000..00b60a2d0c --- /dev/null +++ b/testing/web-platform/tests/reporting/cross-origin-reports-isolated.https.sub.html.sub.headers @@ -0,0 +1,2 @@ +Reporting-Endpoints: csp-endpoint="https://{{domains[www]}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=d0d517bf-891b-457a-b970-8b2b2c81a0bf" +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-endpoint diff --git a/testing/web-platform/tests/reporting/cross-origin-same-site-credentials.https.sub.html b/testing/web-platform/tests/reporting/cross-origin-same-site-credentials.https.sub.html new file mode 100644 index 0000000000..2f3f5fefca --- /dev/null +++ b/testing/web-platform/tests/reporting/cross-origin-same-site-credentials.https.sub.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that credentials are sent properly in a cross-origin but same-site nested context</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> +<body> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = 'd0d517bf-891b-457a-b970-8b2b2c81a0bf'; + + promise_test(async t => { + // If this is not run from the expected origin, then the A->A->www.A frame embedding will not be correct, + // and the cookies set in the top-level page will never be returned with the reports. + assert_true(location.href.startsWith("https://{{hosts[][]}}:{{ports[https][0]}}/"), + "Test running on unexpected origin; subsequent assertions will fail."); + + // Set credentials, and set up test to clear them afterwards. Cookies are set with the Domain + // attribute, so that they may be sent to same-site resources. + await fetch('/cookies/resources/setSameSiteDomain.py?reporting', {mode: 'no-cors', credentials: 'include', cache: 'no-store'}); + t.add_cleanup(() => fetch("/cookies/resources/dropSameSite.py", {mode: 'no-cors', credentials: 'include', cache: 'no-store'})); + + // Insert a same-origin frame, which will then frame a same-site but cross-origin page to + // trigger a CSP error. + const frame = document.createElement('iframe'); + frame.src = "https://{{hosts[][]}}:{{ports[https][0]}}/reporting/resources/middle-frame.https.sub.html?host={{hosts[][www]}}"; + + // Wait for the inner frame to signal that the report has been generated. + await new Promise(resolve => { + window.addEventListener('message', ev => { + if (ev.data === "done") + resolve(ev.data); + }); + document.body.appendChild(frame); + }); + + const reports = await pollReports(endpoint, id); + checkReportExists(reports, 'csp-violation', "https://{{hosts[][www]}}:{{ports[https][0]}}/reporting/resources/same-origin-report.https.sub.html"); + + // All credentials set at the top-level should be received. + const cookies = await pollCookies(endpoint, id); + assert_equals(cookies.samesite_none, "[samesite_none=reporting]", "Credential value was correct"); + assert_equals(cookies.samesite_unspecified, "[samesite_unspecified=reporting]", "Credential value was correct"); + assert_equals(cookies.samesite_lax, "[samesite_lax=reporting]", "Credential value was correct"); + assert_equals(cookies.samesite_strict, "[samesite_strict=reporting]", "Credential value was correct"); + assert_equals(Object.keys(cookies).length, 4, "No additional cookies were received"); + + }, "Reporting endpoints received credentials."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/disconnect.html b/testing/web-platform/tests/reporting/disconnect.html new file mode 100644 index 0000000000..12d33db8ff --- /dev/null +++ b/testing/web-platform/tests/reporting/disconnect.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Reporting: Disconnect</title> +<link rel="author" title="Paul Meyer" href="paulmeyer@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> +promise_test(async test => { + let observer; + const reportsPromise = new Promise(resolve => { + observer = new ReportingObserver(resolve); + observer.observe(); + }); + + // The observer should still receive this report even though disconnect() + // is called immediately afterwards. + await test_driver.generate_test_report("Test message.") + .then(() => { observer.disconnect(); }); + + const reports = await reportsPromise; + assert_equals(reports.length, 1); + assert_equals(reports[0].body.message, "Test message."); + }, "Disconnect"); +</script> diff --git a/testing/web-platform/tests/reporting/document-reporting-bypass-report-to.https.sub.html b/testing/web-platform/tests/reporting/document-reporting-bypass-report-to.https.sub.html new file mode 100644 index 0000000000..394bc9e40a --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-bypass-report-to.https.sub.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Test that reports ignore Report-To header when Reporting-Endpoints is configured</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> + +<body> + <script> + promise_test(async t => { + return new Promise(resolve => { + new ReportingObserver((reports, observer) => resolve(reports), + { types: ['document-policy-violation'] }).observe(); + }).then((reports) => { + assert_equals(reports[0].type, 'document-policy-violation'); + }) + }, "document policy violation observed"); + </script> + <script>document.write("This should be written into the document");</script> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const report_to_id = 'caddb022-90ea-48e8-a675-4cebaf7e8388'; + const reporting_endpoints_id = '6c2131d0-1e9b-4ee8-a196-952f2ae4ae97'; + promise_test(async t => { + await wait(3000); + // Verify no reports sent to Report-To endpoint + let reports = await pollReports(endpoint, report_to_id); + assert_equals(reports.length, 0); + // Verify report is received on Reporting-Endpoints endpoint + reports = await pollReports(endpoint, reporting_endpoints_id); + checkReportExists(reports, 'document-policy-violation', location.href); + }, "Only the Reporting-Endpoints configured endpoint received reports."); + </script> + +</body> + +</html> diff --git a/testing/web-platform/tests/reporting/document-reporting-bypass-report-to.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/document-reporting-bypass-report-to.https.sub.html.sub.headers new file mode 100644 index 0000000000..b2a3d20f48 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-bypass-report-to.https.sub.html.sub.headers @@ -0,0 +1,3 @@ +Reporting-Endpoints: group1="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=6c2131d0-1e9b-4ee8-a196-952f2ae4ae97" +Report-To: { "group": "group1", "max_age": 10886400, "endpoints": [{ "url": "/reporting/resources/report.py?reportID=caddb022-90ea-48e8-a675-4cebaf7e8388" }] } +Document-Policy-Report-Only: document-write=?0;report-to=group1 diff --git a/testing/web-platform/tests/reporting/document-reporting-default-endpoint.https.sub.html b/testing/web-platform/tests/reporting/document-reporting-default-endpoint.https.sub.html new file mode 100644 index 0000000000..f1951e3469 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-default-endpoint.https.sub.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Test that document level reports are sent to default endpoint</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src='resources/report-helper.js'></script> +<p id="error">No error</p> +<script> + async_test(function (test) { + var observer = new ReportingObserver(function (reports) { + test.step(function () { + assert_equals(reports.length, 1); + assert_equals(reports[0].type, "deprecation"); + }); + test.done(); + }); + observer.observe(); + }, "report generated"); +</script> +<script>webkitRequestAnimationFrame(() => {});</script> +<script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = '46ecac28-6d27-4763-a692-bcc588054716'; + promise_test(async t => { + await wait(3000); + const reports = await pollReports(endpoint, id); + checkReportExists(reports, 'deprecation', location.href); + }, "Reporting-Endpoints defined endpoint received reports."); +</script> +</body> + +</html> diff --git a/testing/web-platform/tests/reporting/document-reporting-default-endpoint.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/document-reporting-default-endpoint.https.sub.html.sub.headers new file mode 100644 index 0000000000..e374d79be0 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-default-endpoint.https.sub.html.sub.headers @@ -0,0 +1 @@ +Reporting-Endpoints: default="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=46ecac28-6d27-4763-a692-bcc588054716" diff --git a/testing/web-platform/tests/reporting/document-reporting-destroy-after-document-close.https.sub.html b/testing/web-platform/tests/reporting/document-reporting-destroy-after-document-close.https.sub.html new file mode 100644 index 0000000000..e6ec91ade3 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-destroy-after-document-close.https.sub.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Test that reports are not sent without Reporting-Endpoints header, with previous header set on same URL</title> + <script src="/common/utils.js"></script> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> + +<body> + <iframe name="test"></iframe> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const report_id = token(); + const document_url = + `resources/generate-report-once.py?reportID=${report_id}`; + promise_test(async t => { + // Load a document that generates report into iframe. Server should return + // Reporting-Endpoints header. + const w = window.open(document_url, "test"); + let reports = await pollReports(endpoint, report_id); + // Verify that reporting is configured on the document. + assert_equals(reports.length, 1); + // reload opened window. This time server will not return + // Reporting-Endpoints header. + w.location.reload(); + reports = await pollReports(endpoint, report_id); + // Verify no reports are sent this time. + assert_equals(reports.length, 0); + + }, "No more reports received after navigation to same document without endpoint header"); + </script> + +</body> + +</html> diff --git a/testing/web-platform/tests/reporting/document-reporting-named-endpoints.https.sub.html b/testing/web-platform/tests/reporting/document-reporting-named-endpoints.https.sub.html new file mode 100644 index 0000000000..c24601147a --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-named-endpoints.https.sub.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Test that reports are sent to multiple named endpoints</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> + +<body> + <script> + const t = async_test("Test that image does not load"); + async_test(function (t) { + const observer = new ReportingObserver((reports, observer) => { + t.step(() => { + assert_equals(reports[0].type, 'csp-violation'); + }); + t.done(); + }, { types: ['csp-violation'] }); + observer.observe(); + }, "csp violation report observed"); + + promise_test(async t => { + return new Promise(resolve => { + new ReportingObserver((reports, observer) => resolve(reports), + { types: ['document-policy-violation'] }).observe(); + }).then((reports) => { + assert_equals(reports[0].type, 'document-policy-violation'); + }) + }, "document policy violation observed"); + </script> + <img src='/reporting/resources/fail.png' onload='t.unreached_func("The image should not have loaded");' + onerror='t.done();'> + <script>document.write("This should be written into the document");</script> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const group1_id = '0d334af1-1c5c-4e59-9079-065131ff2a45'; + const group2_id = '09c1a265-5fc7-4c49-b35c-32078c2d0c19'; + promise_test(async t => { + await wait(3000); + // Verify CSP reports are sent to configured endpoint. + const csp_reports = await pollReports(endpoint, group1_id); + checkReportExists(csp_reports, 'csp-violation', location.href); + // Verify Document Policy reports are sent to configured endpoint. + const dp_reports = await pollReports(endpoint, group2_id); + checkReportExists(dp_reports, 'document-policy-violation', location.href); + }, "Reporting endpoints received reports."); + </script> + +</body> + +</html> diff --git a/testing/web-platform/tests/reporting/document-reporting-named-endpoints.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/document-reporting-named-endpoints.https.sub.html.sub.headers new file mode 100644 index 0000000000..2d5a308db2 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-named-endpoints.https.sub.html.sub.headers @@ -0,0 +1,4 @@ +Reporting-Endpoints: group1="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=0d334af1-1c5c-4e59-9079-065131ff2a45" +Reporting-Endpoints: group2="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=09c1a265-5fc7-4c49-b35c-32078c2d0c19" +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to group1 +Document-Policy-Report-Only: document-write=?0;report-to=group2 diff --git a/testing/web-platform/tests/reporting/document-reporting-not-batch-different-document.https.html b/testing/web-platform/tests/reporting/document-reporting-not-batch-different-document.https.html new file mode 100644 index 0000000000..e124bd7fbd --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-not-batch-different-document.https.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Test that reports are sent to multiple named endpoints</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> + +<body> + <iframe name="report1"></iframe> + <iframe name="report2"></iframe> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const report_id = '204d2fb2-018b-4e35-964c-5e298e89d4e2'; + promise_test(async t => { + const w = window.open(`resources/generate-report.https.sub.html?pipe=header(Reporting-Endpoints,default="/reporting/resources/report.py?reportID=${report_id}")`, "report1"); + const w2 = window.open(`resources/generate-csp-report.https.sub.html?pipe=header(Reporting-Endpoints,default="/reporting/resources/report.py?reportID=${report_id}")`, "report2"); + await wait(3000); + // Verify that each iframe generated and sent one report. + const reports = await pollReports(endpoint, report_id); + assert_equals(reports.length, 2, "Number of reports"); + checkReportExists(reports, 'deprecation', w.location.href); + checkReportExists(reports, 'csp-violation', w2.location.href); + const request_count = await pollNumResults(endpoint, report_id); + // Verify that requests are sent separately. + assert_equals(request_count, 2, "Count of requests"); + }, "Reports are not batched for same url in different document."); + </script> + +</body> + +</html> diff --git a/testing/web-platform/tests/reporting/document-reporting-override-endpoint.https.sub.html b/testing/web-platform/tests/reporting/document-reporting-override-endpoint.https.sub.html new file mode 100644 index 0000000000..9264786093 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-override-endpoint.https.sub.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>Test that Reporting-Endpoints header endpoint with same name override previous value</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> + +<body> + <script> + promise_test(async t => { + return new Promise(resolve => { + new ReportingObserver((reports, observer) => resolve(reports), + { types: ['document-policy-violation'] }).observe(); + }).then((reports) => { + assert_equals(reports[0].type, 'document-policy-violation'); + }) + }, "document policy violation observed"); + </script> + <script>document.write("This should be written into the document");</script> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const first_group1_id = 'b523d7f5-28f0-4be6-9460-e163ee9b4ab8'; + const second_group1_id = '03e4474d-768c-42f2-8e17-39aa95b309e3'; + promise_test(async t => { + await wait(3000); + // Verify that no reports are sent to old header endpoint. + let reports = await pollReports(endpoint, first_group1_id); + assert_equals(reports.length, 0); + // Verify that reports are sent to the new header endpoint. + reports = await pollReports(endpoint, second_group1_id); + checkReportExists(reports, 'document-policy-violation', location.href); + }, "Only the second reporting endpoint received reports."); + </script> + +</body> + +</html> diff --git a/testing/web-platform/tests/reporting/document-reporting-override-endpoint.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/document-reporting-override-endpoint.https.sub.html.sub.headers new file mode 100644 index 0000000000..46954f4d5c --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-override-endpoint.https.sub.html.sub.headers @@ -0,0 +1,3 @@ +Reporting-Endpoints: group1="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=b523d7f5-28f0-4be6-9460-e163ee9b4ab8" +Reporting-Endpoints: group1="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=03e4474d-768c-42f2-8e17-39aa95b309e3" +Document-Policy-Report-Only: document-write=?0;report-to=group1 diff --git a/testing/web-platform/tests/reporting/document-reporting-path-absolute.https.sub.html b/testing/web-platform/tests/reporting/document-reporting-path-absolute.https.sub.html new file mode 100644 index 0000000000..48be010a00 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-path-absolute.https.sub.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Test that Reporting-Endpoints report received for absolute path endpoint.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src='resources/report-helper.js'></script> +<p id="error">No error</p> +<script> + async_test(function (test) { + var observer = new ReportingObserver(function (reports) { + test.step(function () { + // Reports should be received in the same order that they were + // generated. + assert_equals(reports.length, 1); + assert_equals(reports[0].type, "deprecation"); + }); + test.done(); + }); + observer.observe(); + }, "report generated"); +</script> +<script>window.webkitCancelAnimationFrame(() => {});</script> +<script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = '8106c1d6-55f7-4c82-a8e1-fabc59f890f8'; + promise_test(async t => { + await wait(3000); + const reports = await pollReports(endpoint, id); + checkReportExists(reports, 'deprecation', location.href); + }, "Reporting-Endpoints defined endpoint received reports."); +</script> +</body> + +</html> diff --git a/testing/web-platform/tests/reporting/document-reporting-path-absolute.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/document-reporting-path-absolute.https.sub.html.sub.headers new file mode 100644 index 0000000000..547cd6a588 --- /dev/null +++ b/testing/web-platform/tests/reporting/document-reporting-path-absolute.https.sub.html.sub.headers @@ -0,0 +1 @@ +Reporting-Endpoints: default="/reporting/resources/report.py?reportID=8106c1d6-55f7-4c82-a8e1-fabc59f890f8" diff --git a/testing/web-platform/tests/reporting/generateTestReport-honors-endpoint.https.sub.html b/testing/web-platform/tests/reporting/generateTestReport-honors-endpoint.https.sub.html new file mode 100644 index 0000000000..f04f5e00f9 --- /dev/null +++ b/testing/web-platform/tests/reporting/generateTestReport-honors-endpoint.https.sub.html @@ -0,0 +1,51 @@ +<!doctype html> +<html> +<head> + <title>Test that the reporting-api generate_test_report feature honors endpoints</title> + <link rel="author" title="Brent Fulgham" href="bfulgham@apple.com"> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script src='resources/report-helper.js'></script> +</head> +<body> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = '41534b09-65b2-498a-9fd3-104281ed63ce'; + + function checkReportIsValid(reports, type, url) { + for (const report of reports) { + if (report.type !== type) continue; + if (report.url.endsWith("reporting/generateTestReport-honors-endpoint.https.sub.html")) + return true; + } + assert_unreached(`A report of ${type} from ${url} was not found.`); + } + + async_test(function(test) { + var observer = new ReportingObserver(function(reports) { + test.step(function() { + assert_equals(reports.length, 1); + // Ensure that the contents of the report are valid. + assert_equals(reports[0].type, "test"); + assert_true(reports[0].url.endsWith("reporting/generateTestReport-honors-endpoint.https.sub.html")); + assert_equals(reports[0].body.message, "Test message."); + }); + test.done(); + }); + observer.observe(); + + // This should result in a "test" type report being generated and observed. + test_driver.generate_test_report("Test message.") + .catch(test.unreached_func('generate test report failed')); + }, "Generate Test Report"); + + promise_test(async t => { + const reports = await pollReports(endpoint, id); + checkReportIsValid(reports, 'test', location.href); + }, "Reporting-Endpoints target received the test report."); +</script> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/reporting/generateTestReport-honors-endpoint.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/generateTestReport-honors-endpoint.https.sub.html.sub.headers new file mode 100644 index 0000000000..7167d54d83 --- /dev/null +++ b/testing/web-platform/tests/reporting/generateTestReport-honors-endpoint.https.sub.html.sub.headers @@ -0,0 +1 @@ +Reporting-Endpoints: default="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=41534b09-65b2-498a-9fd3-104281ed63ce" diff --git a/testing/web-platform/tests/reporting/generateTestReport.html b/testing/web-platform/tests/reporting/generateTestReport.html new file mode 100644 index 0000000000..1c6e7dc225 --- /dev/null +++ b/testing/web-platform/tests/reporting/generateTestReport.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Reporting: Generate Test Report</title> +<link rel="author" title="Paul Meyer" href="paulmeyer@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> + // Test that the "generate_test_report" API works. + async_test(function(test) { + var observer = new ReportingObserver(function(reports) { + test.step(function() { + assert_equals(reports.length, 1); + // Ensure that the contents of the report are valid. + assert_equals(reports[0].type, "test"); + assert_true(reports[0].url.endsWith("reporting/generateTestReport.html")); + assert_equals(reports[0].body.message, "Test message."); + // Ensure that the toJSON() call of the report are valid. + const reportJSON = reports[0].toJSON(); + assert_equals(reports[0].type, reportJSON.type); + assert_equals(reports[0].url, reportJSON.url); + assert_equals(reports[0].body.message, reportJSON.body.message); + // Ensure that report can be successfully JSON serialized. + assert_equals(JSON.stringify(reports[0]), JSON.stringify(reportJSON)); + }); + test.done(); + }); + observer.observe(); + + // This should result in a "test" type report being generated and observed. + test_driver.generate_test_report("Test message.") + .catch(test.unreached_func('generate test report failed')); + }, "Generate Test Report"); +</script> diff --git a/testing/web-platform/tests/reporting/idlharness.any.js b/testing/web-platform/tests/reporting/idlharness.any.js new file mode 100644 index 0000000000..17cef81835 --- /dev/null +++ b/testing/web-platform/tests/reporting/idlharness.any.js @@ -0,0 +1,14 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +idl_test( + ['reporting'], + [], + idl_array => { + idl_array.add_objects({ + // TODO: objects + }); + } +); diff --git a/testing/web-platform/tests/reporting/nestedReport.html b/testing/web-platform/tests/reporting/nestedReport.html new file mode 100644 index 0000000000..156338ee74 --- /dev/null +++ b/testing/web-platform/tests/reporting/nestedReport.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Reporting: Nested report</title> +<link rel="author" title="Paul Meyer" href="paulmeyer@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> + // Test that reports can be generated within a ReportingObserver + // callback. These reports should be received by the same observer. + async_test(function(test) { + var step = 0; + var observer = new ReportingObserver(async function(reports, observer) { + test.step(function() { + assert_equals(reports.length, 1); + assert_equals(reports[0].body.message, "" + step); + }); + + ++step; + if (step == 3) + test.done(); + + test_driver.generate_test_report("" + step); + }); + observer.observe(); + + test_driver.generate_test_report("0"); + }, "Nested report"); +</script> diff --git a/testing/web-platform/tests/reporting/order.html b/testing/web-platform/tests/reporting/order.html new file mode 100644 index 0000000000..c43964220c --- /dev/null +++ b/testing/web-platform/tests/reporting/order.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Reporting: Order</title> +<link rel="author" title="Paul Meyer" href="paulmeyer@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<p id="error">No error</p> +<script> + var count = 0; + async_test(function(test) { + var observer = new ReportingObserver(function(reports) { + test.step(function() { + // Reports should be received in the same order that they were + // generated. + for(i in reports) { + assert_equals(reports[i].body.message, "" + count++); + } + }); + + if (count == 10) + test.done(); + }); + observer.observe(); + + for (i = 0; i != 10; ++i) + test_driver.generate_test_report("" + i); + }, "Order"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/reporting-api-honors-limits.https.sub.html b/testing/web-platform/tests/reporting/reporting-api-honors-limits.https.sub.html new file mode 100644 index 0000000000..4732711888 --- /dev/null +++ b/testing/web-platform/tests/reporting/reporting-api-honors-limits.https.sub.html @@ -0,0 +1,70 @@ +<!doctype html> +<html> +<head> + <title>Test that the report-api honors buffer limits on a per-report type basis</title> + <link rel="author" title="Brent Fulgham" href="bfulgham@apple.com"> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> +</head> +<body> + <script> + var t1 = async_test("Test that image does not load"); + + promise_test(async function() { + for (let i = 0; i < 110; ++i) + await test_driver.generate_test_report("" + i); + }, "Buffer filled"); + + async_test(function(t2) { + window.addEventListener("securitypolicyviolation", t2.step_func(function(e) { + assert_equals(e.blockedURI, "{{location[scheme]}}://{{location[host]}}/reporting/support/fail.png"); + assert_equals(e.violatedDirective, "img-src"); + t2.done(); + })); + }, "Event is fired"); + + promise_test(async function(test) { + const cspReports = await new Promise(resolve => { + let observer = new ReportingObserver(resolve, {types:["csp-violation"], buffered:true}); + observer.observe(); + }); + + // WebKit generates two CSP reports for the blocked image load (https://bugs.webkit.org/show_bug.cgi?id=153162) + assert_true(cspReports.length > 0 && cspReports.length < 3); + + // Ensure that the contents of the report are valid. + assert_equals(cspReports[0].type, "csp-violation"); + }, "CSP Report limits were honored"); + + promise_test(async function(test) { + const testReports = await new Promise(resolve => { + let observer = new ReportingObserver(resolve, {types:["test"], buffered:true}); + observer.observe(); + }); + + assert_equals(testReports.length, 100); + + for (let i = 0; i < 100; ++i) { + assert_equals(testReports[i].type, "test"); + assert_equals(testReports[i].body.message, "" + (i + 10)); + } + }, "Test Report limits were honored"); + + promise_test(async function(test) { + const allReports = await new Promise(resolve => { + let observer = new ReportingObserver(resolve, {buffered:true}); + observer.observe(); + }); + + // WebKit generates two CSP reports for the blocked image load (https://bugs.webkit.org/show_bug.cgi?id=153162) + // Other browsers produce only one. + assert_true(allReports.length >= 101 && allReports.length <= 102); + }, "Combined report limits were honored"); + </script> + <img src='/reporting/support/fail.png' + onload='t1.unreached_func("The image should not have loaded");' + onerror='t1.done();'> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/reporting-api-honors-limits.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/reporting-api-honors-limits.https.sub.html.sub.headers new file mode 100644 index 0000000000..376788443e --- /dev/null +++ b/testing/web-platform/tests/reporting/reporting-api-honors-limits.https.sub.html.sub.headers @@ -0,0 +1,7 @@ +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 +Set-Cookie: reporting-api-honors-limits={{$id:uuid()}}; Path=/reporting +Reporting-Endpoints: csp-group="https://{{host}}:{{ports[https][0]}}/reporting/resources/report.py?op=put&reportID={{$id}}" +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-group diff --git a/testing/web-platform/tests/reporting/reporting-isolated-across-navigations.https.sub.html b/testing/web-platform/tests/reporting/reporting-isolated-across-navigations.https.sub.html new file mode 100644 index 0000000000..df61afa833 --- /dev/null +++ b/testing/web-platform/tests/reporting/reporting-isolated-across-navigations.https.sub.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Bug test page 1</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/report-helper.js"></script> +<script> +promise_test(async t => { + await new Promise(resolve => { + window.addEventListener("message", resolve); + }); + // At this point, the reporting endpoint should have received all three + // reports. Ensure that reports from the first page are not batched with + // those from the second, or sent to its endpoint. + const csp1_uuid = "112868aa-4b59-57c7-a388-db909ef24295"; + const csp2_uuid = "612bf2ee-b9b8-5f8d-a239-0981c6ff057e"; + const reports1 = await pollReports('/reporting/resources/report.py', csp1_uuid); + const reports2 = await pollReports('/reporting/resources/report.py', csp2_uuid); + + const url_prefix = "https://{{location[host]}}/reporting/resources/"; + + // Validate that both received reports were CSP img-src violations from the + // same reporting source. Each image should be represented once, although the + // order does not matter. + + assert_equals(reports1.length, 2, "First endpoint should receive two reports"); + + assert_equals(reports1[0].type, "csp-violation"); + assert_equals(reports1[0].url, url_prefix + "first-csp-report.https.sub.html"); + assert_equals(reports1[0].body.disposition, "enforce"); + assert_equals(reports1[0].body.effectiveDirective, "img-src"); + + assert_equals(reports1[1].type, "csp-violation"); + assert_equals(reports1[1].url, url_prefix + "first-csp-report.https.sub.html"); + assert_equals(reports1[1].body.disposition, "enforce"); + assert_equals(reports1[1].body.effectiveDirective, "img-src"); + + var image_sources = [reports1[0].body.blockedURL, reports1[1].body.blockedURL].sort(); + assert_equals(image_sources[0], url_prefix + "missing1.png"); + assert_equals(image_sources[1], url_prefix + "missing2.png"); + + // Validate that the report received from the second endpoint was also a CSP + // img-source violation, from a different URL. + + assert_equals(reports2.length, 1, "Second endpoint should reecive one report"); + assert_equals(reports2[0].type, "csp-violation"); + assert_equals(reports2[0].url, url_prefix + "second-csp-report.https.sub.html"); + assert_equals(reports2[0].body.disposition, "enforce"); + assert_equals(reports2[0].body.effectiveDirective, "img-src"); + assert_equals(reports2[0].body.blockedURL, url_prefix + "missing3.png"); +}, "Reports should be sent to the correct endpoints"); +</script> +<body> +<h1>Bug test main frame</h1> +<iframe id="frame" src="resources/first-csp-report.https.sub.html"></iframe> +</body> diff --git a/testing/web-platform/tests/reporting/resources/README.md b/testing/web-platform/tests/reporting/resources/README.md new file mode 100644 index 0000000000..b77d1f96b7 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/README.md @@ -0,0 +1,75 @@ +# Using the common report collector + +To send reports to the collector, configure the reporting API to POST reports +to the collector's URL. This can be same- or cross- origin with the reporting +document, as the collector will follow the CORS protocol. + +The collector supports both CSP Level 2 (report-uri) reports as well as +Reporting API reports. + +A GET request can be used to retrieve stored reports for analysis. + +A POST request can be used to clear reports stored in the server. + +Sent credentials are stored with the reports, and can be retrieved separately. + +CORS Notes: +* Preflight requests originating from www2.web-platform.test will be rejected. + This allows tests to ensure that cross-origin report uploads are not sent when + the endpoint does not support CORS. + +## Supported GET parameters: + `op`: For GET requests, a string indicating the operation to perform (see + below for description of supported operations). Defaults to + `retrieve_report`. + + `reportID`: A UUID to associate with the reports sent from this document. This + can be used to distinguish between reports from multiple documents, and to + provide multiple distinct endpoints for a single document. Either `reportID` + or `endpoint` must be provided. + + `endpoint`: A string which will be used to generate a UUID to be used as the + reportID. Either `reportID` or `endpoint` must be provided. + + `timeout`: The amount of time to wait, in seconds, before responding. Defaults + to 0.5s. + + `min_count`: The minimum number of reports to return with the `retrieve_report` + operation. If there have been fewer than this many reports received, then an + empty report list will be returned instead. + + `retain`: If present, reports will remain in the stash after being retrieved. + By default, reports are cleared once retrieved. + +### Operations: + `retrieve_report`: Returns all reports received so far for this reportID, as a + JSON-formatted list. If no reports have been received, an empty list will be + returned. + + `retrieve_cookies`: Returns the cookies sent with the most recent reports for + this reportID, as a JSON-formatted object. + + `retrieve_count`: Returns the number of POST requests for reports with this + reportID so far. + +## Supported POST JSON payload: + + `op`: For POST requests, a string indicating the operation to perform (see + below for description of supported operations). + + `reportIDs`: A list of `reportID`s, each one a UUID associated with reports stored in the server stash. + +### Operations +`DELETE`: Clear all reports associated with `reportID` listed in `reportIDs` list. + +### Example usage: +``` +# Clear reports on the server. +fetch('/reporting/resources/report.py', { + method: "POST", + body: JSON.stringify({ + op: "DELETE", + reportIDs: [...] # a list of reportID + }) +}); +``` diff --git a/testing/web-platform/tests/reporting/resources/csp-error.https.sub.html b/testing/web-platform/tests/reporting/resources/csp-error.https.sub.html new file mode 100644 index 0000000000..c883051945 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/csp-error.https.sub.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Notify parent on load and generate a CSP error</title> +</head> +<body> + <script> + addEventListener('load', () => { + // Alert the parent frame that this frame has loaded. + parent.postMessage('Loaded','*'); + + // Trigger a CSP error, which should generate a report. + const img = document.createElement('img'); + img.src = "/reporting/resources/fail.png"; + document.body.appendChild(img); + }); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/resources/csp-error.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/resources/csp-error.https.sub.html.sub.headers new file mode 100644 index 0000000000..00b60a2d0c --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/csp-error.https.sub.html.sub.headers @@ -0,0 +1,2 @@ +Reporting-Endpoints: csp-endpoint="https://{{domains[www]}}:{{ports[https][0]}}/reporting/resources/report.py?reportID=d0d517bf-891b-457a-b970-8b2b2c81a0bf" +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-endpoint diff --git a/testing/web-platform/tests/reporting/resources/fail.png b/testing/web-platform/tests/reporting/resources/fail.png Binary files differnew file mode 100644 index 0000000000..b593380333 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/fail.png diff --git a/testing/web-platform/tests/reporting/resources/first-csp-report.https.sub.html b/testing/web-platform/tests/reporting/resources/first-csp-report.https.sub.html new file mode 100644 index 0000000000..9887769128 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/first-csp-report.https.sub.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<title>Bug test page 1</title> +</head> +<body> +<h1>Bug test page 1</h1> +<!-- This image will cause a CSP violation, which will trigger an immediate report --> +<img src="missing1.png"> +<script> +setTimeout(()=>{ + var img = document.createElement('img'); + img.src = "missing2.png"; + // Appending this image will cause a second CSP violation, which will trigger + // a second report. + document.body.appendChild(img); + location.href = "second-csp-report.https.sub.html"; +}, 1); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/reporting/resources/first-csp-report.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/resources/first-csp-report.https.sub.html.sub.headers new file mode 100644 index 0000000000..2f5eeb4d8c --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/first-csp-report.https.sub.html.sub.headers @@ -0,0 +1,2 @@ +Reporting-Endpoints: csp="/reporting/resources/report.py?pipe=trickle(d1)&endpoint=csp1" +Content-Security-Policy: img-src 'none'; report-to csp diff --git a/testing/web-platform/tests/reporting/resources/generate-csp-report.https.sub.html b/testing/web-platform/tests/reporting/resources/generate-csp-report.https.sub.html new file mode 100644 index 0000000000..7adec0f309 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/generate-csp-report.https.sub.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Generate CSP reports </title> +<img src='/reporting/resources/fail.png'> diff --git a/testing/web-platform/tests/reporting/resources/generate-csp-report.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/resources/generate-csp-report.https.sub.html.sub.headers new file mode 100644 index 0000000000..44242c3c89 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/generate-csp-report.https.sub.html.sub.headers @@ -0,0 +1 @@ +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to default diff --git a/testing/web-platform/tests/reporting/resources/generate-report-once.py b/testing/web-platform/tests/reporting/resources/generate-report-once.py new file mode 100644 index 0000000000..163846a4b9 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/generate-report-once.py @@ -0,0 +1,34 @@ +def main(request, response): + # Handle CORS preflight requests + if request.method == u'OPTIONS': + # Always reject preflights for one subdomain + if b"www2" in request.headers[b"Origin"]: + return (400, [], u"CORS preflight rejected for www2") + return [ + (b"Content-Type", b"text/plain"), + (b"Access-Control-Allow-Origin", b"*"), + (b"Access-Control-Allow-Methods", b"get"), + (b"Access-Control-Allow-Headers", b"Content-Type"), + ], u"CORS allowed" + + if b"reportID" in request.GET: + key = request.GET.first(b"reportID") + else: + response.status = 400 + return "reportID parameter is required." + + with request.server.stash.lock: + visited = request.server.stash.take(key=key) + if visited is None: + response.headers.set("Reporting-Endpoints", + b"default=\"/reporting/resources/report.py?reportID=%s\"" % key) + request.server.stash.put(key=key, value=True) + + response.content = b""" +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Generate deprecation report</title> +<script> + webkitRequestAnimationFrame(() => {}); +</script> +""" diff --git a/testing/web-platform/tests/reporting/resources/generate-report.https.sub.html b/testing/web-platform/tests/reporting/resources/generate-report.https.sub.html new file mode 100644 index 0000000000..f1f4e96300 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/generate-report.https.sub.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<meta charset=utf-8> +<title>Generate deprecation report</title> +<script> + webkitRequestAnimationFrame(() => {}); +</script> diff --git a/testing/web-platform/tests/reporting/resources/middle-frame.https.sub.html b/testing/web-platform/tests/reporting/resources/middle-frame.https.sub.html new file mode 100644 index 0000000000..0dd26ecc2b --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/middle-frame.https.sub.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Utility page which embeds a reporting page on the chosen host</title> +</head> +<body> + <script> + const searchParams = new URLSearchParams(window.location.search); + const host = searchParams.get('host') || "{{hosts[][]}}"; + const frame = document.createElement('iframe'); + frame.src=`https://${host}:{{ports[https][0]}}/reporting/resources/same-origin-report.https.sub.html`; + document.body.appendChild(frame); + </script> +</body> +</html>
\ No newline at end of file diff --git a/testing/web-platform/tests/reporting/resources/report-helper.js b/testing/web-platform/tests/reporting/resources/report-helper.js new file mode 100644 index 0000000000..213a635c49 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/report-helper.js @@ -0,0 +1,38 @@ +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +async function pollReports(endpoint, id, min_count) { + const res = await fetch(`${endpoint}?reportID=${id}${min_count ? `&min_count=${min_count}` : ''}`, { cache: 'no-store' }); + const reports = []; + if (res.status === 200) { + for (const report of await res.json()) { + reports.push(report); + } + } + return reports; +} + +async function pollCookies(endpoint, id) { + const res = await fetch(`${endpoint}?reportID=${id}&op=retrieve_cookies`, { cache: 'no-store' }); + const dict = await res.json(); + if (dict.reportCookies == 'None') + return {}; + return dict.reportCookies; +} + +async function pollNumResults(endpoint, id) { + const res = await fetch(`${endpoint}?reportID=${id}&op=retrieve_count`, { cache: 'no-store' }); + const dict = await res.json(); + if (dict.report_count == 'None') + return 0; + return dict.report_count; +} + +function checkReportExists(reports, type, url) { + for (const report of reports) { + if (report.type !== type) continue; + if (report.body.documentURL == url || report.body.sourceFile === url) return true; + } + assert_unreached(`A report of ${type} from ${url} is not found.`); +} diff --git a/testing/web-platform/tests/reporting/resources/report.py b/testing/web-platform/tests/reporting/resources/report.py new file mode 100644 index 0000000000..b5ee0c07d8 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/report.py @@ -0,0 +1,143 @@ +import time +import json +import re +import uuid + +from wptserve.utils import isomorphic_decode + + +def retrieve_from_stash(request, key, timeout, default_value, min_count=None, retain=False): + """Retrieve the set of reports for a given report ID. + + This will extract either the set of reports, credentials, or request count + from the stash (depending on the key passed in) and return it encoded as JSON. + + When retrieving reports, this will not return any reports until min_count + reports have been received. + + If timeout seconds elapse before the requested data can be found in the stash, + or before at least min_count reports are received, default_value will be + returned instead.""" + t0 = time.time() + while time.time() - t0 < timeout: + time.sleep(0.5) + with request.server.stash.lock: + value = request.server.stash.take(key=key) + if value is not None: + have_sufficient_reports = ( + min_count is None or len(value) >= min_count) + if retain or not have_sufficient_reports: + request.server.stash.put(key=key, value=value) + if have_sufficient_reports: + return json.dumps(value) + + return default_value + + +def main(request, response): + # Handle CORS preflight requests + if request.method == u'OPTIONS': + # Always reject preflights for one subdomain + if b"www2" in request.headers[b"Origin"]: + return (400, [], u"CORS preflight rejected for www2") + return [ + (b"Content-Type", b"text/plain"), + (b"Access-Control-Allow-Origin", b"*"), + (b"Access-Control-Allow-Methods", b"post"), + (b"Access-Control-Allow-Headers", b"Content-Type"), + ], u"CORS allowed" + + # Delete reports as requested + if request.method == u'POST': + body = json.loads(request.body) + if (isinstance(body, dict) and "op" in body): + if body["op"] == "DELETE" and "reportIDs" in body: + with request.server.stash.lock: + for key in body["reportIDs"]: + request.server.stash.take(key=key) + return "reports cleared" + response.status = 400 + return "op parameter value not recognized" + + if b"reportID" in request.GET: + key = request.GET.first(b"reportID") + elif b"endpoint" in request.GET: + key = uuid.uuid5(uuid.NAMESPACE_OID, isomorphic_decode( + request.GET[b'endpoint'])).urn.encode('ascii')[9:] + else: + response.status = 400 + return "Either reportID or endpoint parameter is required." + + # Cookie and count keys are derived from the report ID. + cookie_key = re.sub(b'^....', b'cccc', key) + count_key = re.sub(b'^....', b'dddd', key) + + if request.method == u'GET': + try: + timeout = float(request.GET.first(b"timeout")) + except: + timeout = 0.5 + try: + min_count = int(request.GET.first(b"min_count")) + except: + min_count = 1 + retain = (b"retain" in request.GET) + + op = request.GET.first(b"op", b"") + if op in (b"retrieve_report", b""): + return [(b"Content-Type", b"application/json")], retrieve_from_stash(request, key, timeout, u'[]', min_count, retain) + + if op == b"retrieve_cookies": + return [(b"Content-Type", b"application/json")], u"{ \"reportCookies\" : " + str(retrieve_from_stash(request, cookie_key, timeout, u"\"None\"")) + u"}" + + if op == b"retrieve_count": + return [(b"Content-Type", b"application/json")], u"{ \"report_count\": %s }" % retrieve_from_stash(request, count_key, timeout, 0) + + response.status = 400 + return "op parameter value not recognized." + + # Save cookies. + if len(request.cookies.keys()) > 0: + # Convert everything into strings and dump it into a dict. + temp_cookies_dict = {} + for dict_key in request.cookies.keys(): + temp_cookies_dict[isomorphic_decode(dict_key)] = str( + request.cookies.get_list(dict_key)) + with request.server.stash.lock: + # Clear any existing cookie data for this request before storing new data. + request.server.stash.take(key=cookie_key) + request.server.stash.put(key=cookie_key, value=temp_cookies_dict) + + # Append new report(s). + new_reports = json.loads(request.body) + + # If the incoming report is a CSP report-uri report, then it will be a single + # dictionary rather than a list of reports. To handle this case, ensure that + # any non-list request bodies are wrapped in a list. + if not isinstance(new_reports, list): + new_reports = [new_reports] + + for report in new_reports: + report[u"metadata"] = { + u"content_type": isomorphic_decode(request.headers[b"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) + + # Increment report submission count. This tracks the number of times this + # reporting endpoint was contacted, rather than the total number of reports + # submitted, which can be seen from the length of the report list. + with request.server.stash.lock: + count = request.server.stash.take(key=count_key) + if count is None: + count = 0 + count += 1 + request.server.stash.put(key=count_key, value=count) + + # Return acknowledgement report. + return [(b"Content-Type", b"text/plain")], b"Recorded report " + request.body diff --git a/testing/web-platform/tests/reporting/resources/same-origin-report.https.sub.html b/testing/web-platform/tests/reporting/resources/same-origin-report.https.sub.html new file mode 100644 index 0000000000..326a0fd0ed --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/same-origin-report.https.sub.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Generates a CSP violation report and sends it to a same-origin endpoint</title> +</head> +<body> + <script> + const img = document.createElement('img'); + img.src = "/reporting/resources/fail.png"; + document.body.appendChild(img); + // Post back to the main frame that the report should have been queued. + top.postMessage("done", "*"); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/resources/same-origin-report.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/resources/same-origin-report.https.sub.html.sub.headers new file mode 100644 index 0000000000..8244fafb22 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/same-origin-report.https.sub.html.sub.headers @@ -0,0 +1,2 @@ +Reporting-Endpoints: csp-endpoint="/reporting/resources/report.py?reportID=d0d517bf-891b-457a-b970-8b2b2c81a0bf" +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-endpoint diff --git a/testing/web-platform/tests/reporting/resources/second-csp-report.https.sub.html b/testing/web-platform/tests/reporting/resources/second-csp-report.https.sub.html new file mode 100644 index 0000000000..a34bc653f5 --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/second-csp-report.https.sub.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> +<title>Bug test page 2</title> +</head> +<body> +<h1>Bug test page 2</h1> +<script> +var img = document.createElement('img'); +img.src = "missing3.png"; +// Appending this image will cause a third CSP violation. The report generated +// here must not be batched with the reports from the previous page, regardless +// of whether they have been sent or not. +document.body.appendChild(img); +// Give the report handler enough time to finish handling any reports from the +// previous page (Reports there are delayed by 1 second because of the trickle +// pipe in the headers in first-csp-report.https.sub.html.sub.headers) and then +// inform the parent that reports may be checked. +setTimeout(()=>{ + parent.postMessage("ready", "*"); +},1250); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/resources/second-csp-report.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/resources/second-csp-report.https.sub.html.sub.headers new file mode 100644 index 0000000000..b32c548fef --- /dev/null +++ b/testing/web-platform/tests/reporting/resources/second-csp-report.https.sub.html.sub.headers @@ -0,0 +1,2 @@ +Reporting-Endpoints: csp="/reporting/resources/report.py?endpoint=csp2" +Content-Security-Policy: img-src 'none'; report-to csp diff --git a/testing/web-platform/tests/reporting/same-origin-cross-site-credentials.https.sub.html b/testing/web-platform/tests/reporting/same-origin-cross-site-credentials.https.sub.html new file mode 100644 index 0000000000..258ab8e103 --- /dev/null +++ b/testing/web-platform/tests/reporting/same-origin-cross-site-credentials.https.sub.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that credentials are sent properly in a same-origin but not same-site context</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> +<body> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = 'd0d517bf-891b-457a-b970-8b2b2c81a0bf'; + + promise_test(async t => { + // If this is not run from the expected origin, then the A->B->A frame embedding will not be correct, + // and the cookies set in the top-level page will never be returned with the reports. + assert_true(location.href.startsWith("https://{{hosts[][]}}:{{ports[https][0]}}/"), + "Test running on unexpected origin; subsequent assertions will fail."); + + // Set credentials, and set up test to clear them afterwards. + await fetch('/cookies/resources/setSameSite.py?reporting', {mode: 'no-cors', credentials: 'include', cache: 'no-store'}); + t.add_cleanup(() => fetch("/cookies/resources/dropSameSite.py", {mode: 'no-cors', credentials: 'include', cache: 'no-store'})); + + // Insert a cross-origin frame which will then frame this origin to + // trigger a CSP error. + const frame = document.createElement('iframe'); + frame.src = "https://{{hosts[alt][]}}:{{ports[https][0]}}/reporting/resources/middle-frame.https.sub.html"; + document.body.appendChild(frame); + + // Wait for the inner frame to signal that the report has been generated. + await new Promise(resolve => { + window.addEventListener('message', ev => { + if (ev.data === "done") + resolve(ev.data); + }); + document.body.appendChild(frame); + }); + + const reports = await pollReports(endpoint, id); + checkReportExists(reports, 'csp-violation', "https://{{hosts[][]}}:{{ports[https][0]}}/reporting/resources/same-origin-report.https.sub.html"); + + // Same-site: None cookies should be sent, but not Lax, Strict, or default cookies. + const cookies = await pollCookies(endpoint, id); + assert_equals(cookies.samesite_none, "[samesite_none=reporting]", "Credential value was correct"); + assert_false("samesite_strict" in cookies, "Same-site: Strict cookies should not be sent"); + assert_false("samesite_lax" in cookies, "Same-site: Lax cookies should not be sent"); + assert_false("samesite_unspecified" in cookies, "Same-site unspecified cookies should not be sent"); + assert_equals(Object.keys(cookies).length, 1, "No additional cookies were received"); + }, "Reporting endpoints received credentials."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/same-origin-report-credentials.https.sub.html b/testing/web-platform/tests/reporting/same-origin-report-credentials.https.sub.html new file mode 100644 index 0000000000..cd93bd601b --- /dev/null +++ b/testing/web-platform/tests/reporting/same-origin-report-credentials.https.sub.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that reports are sent with credentials to same-origin endpoints</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> +<body> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = '320db941-960a-4529-8c4a-24aeb6739309'; + + promise_test(async t => { + // Set credentials, and set up test to clear them afterwards. + await fetch('/cookies/resources/set-cookie.py?name=report&path=%2F', {mode: 'no-cors', credentials: 'include', cache: 'no-store'}); + t.add_cleanup(() => fetch("/cookies/resources/set.py?report=; path=%2F; expires=Thu, 01 Jan 1970 00:00:01 GMT")); + + // Trigger a CSP error. + await new Promise(resolve => { + const img = document.createElement('img'); + img.src = "/reporting/resources/fail.png"; + img.addEventListener('error', resolve); + document.body.appendChild(img); + }); + + // Wait for report to be received. + const reports = await pollReports(endpoint, id); + checkReportExists(reports, 'csp-violation', location.href); + + // Validate that credentials were sent to same-origin endpoint. + const cookies = await pollCookies(endpoint, id); + assert_true('report' in cookies, "Credentials were present in report"); + assert_equals(cookies.report, "[report=1]", "Credential value was correct"); + }, "Reporting endpoints received credentials."); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/reporting/same-origin-report-credentials.https.sub.html.sub.headers b/testing/web-platform/tests/reporting/same-origin-report-credentials.https.sub.html.sub.headers new file mode 100644 index 0000000000..a88efd0cf9 --- /dev/null +++ b/testing/web-platform/tests/reporting/same-origin-report-credentials.https.sub.html.sub.headers @@ -0,0 +1,2 @@ +Reporting-Endpoints: csp-endpoint="/reporting/resources/report.py?reportID=320db941-960a-4529-8c4a-24aeb6739309" +Content-Security-Policy: script-src 'self' 'unsafe-inline'; img-src 'none'; report-to csp-endpoint diff --git a/testing/web-platform/tests/reporting/same-origin-same-site-credentials.https.sub.html b/testing/web-platform/tests/reporting/same-origin-same-site-credentials.https.sub.html new file mode 100644 index 0000000000..9b99edb26e --- /dev/null +++ b/testing/web-platform/tests/reporting/same-origin-same-site-credentials.https.sub.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that credentials are sent properly in a same-origin and also same-site nested context</title> + <script src='/resources/testharness.js'></script> + <script src='/resources/testharnessreport.js'></script> + <script src='resources/report-helper.js'></script> +</head> +<body> + <script> + const base_url = `${location.protocol}//${location.host}`; + const endpoint = `${base_url}/reporting/resources/report.py`; + const id = 'd0d517bf-891b-457a-b970-8b2b2c81a0bf'; + + promise_test(async t => { + // If this is not run from the expected origin, then the A->A->A frame embedding will not be correct, + // and the cookies set in the top-level page will never be returned with the reports. + assert_true(location.href.startsWith("https://{{hosts[][]}}:{{ports[https][0]}}/"), + "Test running on unexpected origin; subsequent assertions will fail."); + + // Set credentials, and set up test to clear them afterwards. + await fetch('/cookies/resources/setSameSite.py?reporting', {mode: 'no-cors', credentials: 'include', cache: 'no-store'}); + t.add_cleanup(() => fetch("/cookies/resources/dropSameSite.py", {mode: 'no-cors', credentials: 'include', cache: 'no-store'})); + + // Insert a same-origin frame which will then frame this origin to + // trigger a CSP error. + const frame = document.createElement('iframe'); + frame.src = "https://{{hosts[][]}}:{{ports[https][0]}}/reporting/resources/middle-frame.https.sub.html"; + document.body.appendChild(frame); + + // Wait for the inner frame to signal that the report has been generated. + await new Promise(resolve => { + window.addEventListener('message', ev => { + if (ev.data === "done") + resolve(ev.data); + }); + document.body.appendChild(frame); + }); + + const reports = await pollReports(endpoint, id); + checkReportExists(reports, 'csp-violation', "https://{{hosts[][]}}:{{ports[https][0]}}/reporting/resources/same-origin-report.https.sub.html"); + + // All credentials set at the top-level should be received. + const cookies = await pollCookies(endpoint, id); + assert_equals(cookies.samesite_none, "[samesite_none=reporting]", "Credential value was correct"); + assert_equals(cookies.samesite_unspecified, "[samesite_unspecified=reporting]", "Credential value was correct"); + assert_equals(cookies.samesite_lax, "[samesite_lax=reporting]", "Credential value was correct"); + assert_equals(cookies.samesite_strict, "[samesite_strict=reporting]", "Credential value was correct"); + assert_equals(Object.keys(cookies).length, 4, "No additional cookies were received"); + }, "Reporting endpoints received credentials."); + </script> +</body> +</html> |