diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/reporting/resources | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/reporting/resources')
17 files changed, 404 insertions, 0 deletions
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 |