From d9acb68005c48f2cc948c2542763ff006a9cb13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A2ris=20Meuleman?= Date: Tue, 19 May 2020 08:18:30 -0700 Subject: [PATCH] Add basic reporting test from Same-origin Bug: 1076456 Change-Id: I7a39d4def20692d8628ce2406569638310684f4f --- .../reporting-coop-navigated-popup.https.html | 109 +++++++++ ...oop-navigated-popup.https.html.sub.headers | 2 + ...ing-popup-same-origin-report-to.https.html | 212 ++++++++++++++++++ ...me-origin-report-to.https.html.sub.headers | 2 + .../reporting-popup-same-origin.https.html | 152 +++++++++++++ ...rting-popup-same-origin.https.html.headers | 1 + .../resources/common.js | 15 +- .../resources/coop-coep.py | 29 +++ .../resources/report.py | 42 ++++ .../resources/reporting-common.js | 153 +++++++++++++ 10 files changed, 710 insertions(+), 7 deletions(-) create mode 100644 html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html create mode 100644 html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin.https.html create mode 100644 html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers create mode 100644 html/cross-origin-opener-policy/resources/report.py create mode 100644 html/cross-origin-opener-policy/resources/reporting-common.js diff --git a/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html new file mode 100644 index 00000000000000..81271a8ab0e6dc --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html @@ -0,0 +1,109 @@ + +Cross-Origin-Opener-Policy: a navigated popup + + + + + + \ No newline at end of file diff --git a/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers new file mode 100644 index 00000000000000..c256aa0babd56b --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-coop-navigated-popup.https.html.sub.headers @@ -0,0 +1,2 @@ +Cross-Origin-Opener-Policy: same-origin-allow-popups; report-to="coop-report-endpoint" +report-to: { "group": "coop-report-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-endpoint" }] }, { "group": "coop-report-only-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-only-endpoint" }]} \ No newline at end of file diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html new file mode 100644 index 00000000000000..9d1aa2173f999a --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html @@ -0,0 +1,212 @@ + + + + +reporting same origin with report-to + + + + + + +
+ diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers new file mode 100644 index 00000000000000..b760be801de16f --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin-report-to.https.html.sub.headers @@ -0,0 +1,2 @@ +report-to: { "group": "coop-report-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-endpoint" }] }, { "group": "coop-report-only-endpoint", "max_age": 10886400, "endpoints": [{ "url": "https://{{hosts[][www]}}:{{ports[https][0]}}/html/cross-origin-opener-policy/resources/report.py?endpoint=coop-report-only-endpoint" }]} +Cross-Origin-Opener-Policy: same-origin; report-to="coop-report-endpoint" \ No newline at end of file diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html new file mode 100644 index 00000000000000..f26ad941f2b5ee --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html @@ -0,0 +1,152 @@ + + + + +reporting same origin + + + + + + +
+ diff --git a/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers new file mode 100644 index 00000000000000..3184e71da46f67 --- /dev/null +++ b/html/cross-origin-opener-policy/reporting-popup-same-origin.https.html.headers @@ -0,0 +1 @@ +Cross-Origin-Opener-Policy: same-origin \ No newline at end of file diff --git a/html/cross-origin-opener-policy/resources/common.js b/html/cross-origin-opener-policy/resources/common.js index fb517e8c40ac56..f23d006c29b143 100644 --- a/html/cross-origin-opener-policy/resources/common.js +++ b/html/cross-origin-opener-policy/resources/common.js @@ -29,11 +29,12 @@ function validate_results(callback, test, w, channelName, hasOpener, openerDOMAc } } -function url_test(t, url, channelName, hasOpener, openerDOMAccess) { +function url_test(t, url, channelName, hasOpener, openerDOMAccess, callback) { + if (callback === undefined) callback = () => { t.done(); }; const bc = new BroadcastChannel(channelName); bc.onmessage = t.step_func(event => { const payload = event.data; - validate_results(() => { t.done(); }, t, w, channelName, hasOpener, openerDOMAccess, payload); + validate_results(callback, t, w, channelName, hasOpener, openerDOMAccess, payload); }); const w = window.open(url, channelName); @@ -46,12 +47,12 @@ function url_test(t, url, channelName, hasOpener, openerDOMAccess) { }); } -function coop_coep_test(t, host, coop, coep, channelName, hasOpener, openerDOMAccess) { - url_test(t, `${host.origin}/html/cross-origin-opener-policy/resources/coop-coep.py?coop=${encodeURIComponent(coop)}&coep=${coep}&channel=${channelName}`, channelName, hasOpener, openerDOMAccess); +function coop_coep_test(t, host, coop, coep, channelName, hasOpener, openerDOMAccess, callback) { + url_test(t, `${host.origin}/html/cross-origin-opener-policy/resources/coop-coep.py?coop=${encodeURIComponent(coop)}&coep=${coep}&channel=${channelName}`, channelName, hasOpener, openerDOMAccess, callback); } -function coop_test(t, host, coop, channelName, hasOpener) { - coop_coep_test(t, host, coop, "", channelName, hasOpener); +function coop_test(t, host, coop, channelName, hasOpener, callback) { + coop_coep_test(t, host, coop, "", channelName, hasOpener, undefined /* openerDOMAccess */, callback); } function run_coop_tests(documentCOOPValueTitle, testArray) { @@ -59,7 +60,7 @@ function run_coop_tests(documentCOOPValueTitle, testArray) { async_test(t => { coop_test(t, test[0], test[1], `${documentCOOPValueTitle}_to_${test[0].name}_${test[1].replace(/ /g,"-")}`, - test[2]); + test[2], () => { t.done(); }); }, `${documentCOOPValueTitle} document opening popup to ${test[0].origin} with COOP: "${test[1]}"`); } } diff --git a/html/cross-origin-opener-policy/resources/coop-coep.py b/html/cross-origin-opener-policy/resources/coop-coep.py index 8a7e0bc0a838fd..03fe6ccb1b96ce 100644 --- a/html/cross-origin-opener-policy/resources/coop-coep.py +++ b/html/cross-origin-opener-policy/resources/coop-coep.py @@ -1,13 +1,42 @@ +def get_reporting_group(host, endpoint): + return '\ +{{\ + "group": "{endpoint}",\ + "max_age": 10886400,\ + "endpoints":\ + [{{\ + "url": "https://{host}/html/cross-origin-opener-policy/resources/report.py?endpoint={endpoint}"\ + }}]\ +}}'.format(host=host, endpoint=endpoint) + def main(request, response): coop = request.GET.first("coop") + coopReportOnly = request.GET.first("coop-report-only") if "coop-report-only" in request.GET else "" coep = request.GET.first("coep") + coepReportOnly = request.GET.first("coep-report-only") if "coep-report-only" in request.GET else "" redirect = request.GET.first("redirect", None) if coop != "": response.headers.set("Cross-Origin-Opener-Policy", coop) + if coop != "": + response.headers.set("Cross-Origin-Opener-Policy-Report-Only", coopReportOnly) if coep != "": response.headers.set("Cross-Origin-Embedder-Policy", coep) + if coep != "": + response.headers.set("Cross-Origin-Embedder-Policy-Report-Only", coepReportOnly) if 'cache' in request.GET: response.headers.set('Cache-Control', 'max-age=3600') + host = request.url_parts[1] + + # add all possible reporting endpoints to the report-to header + # Note that this also returns the coop-report-endpoint, as it may override + # the test's endpoints if same-origin. + response.headers.set('report-to', + get_reporting_group(host, "coop-report-endpoint") + ',' + + get_reporting_group(host, "coop-report-only-endpoint") + ',' + + get_reporting_group(host, "coop-redirect-report-endpoint") + ',' + + get_reporting_group(host, "coop-redirect-report-only-endpoint") + ',' + + get_reporting_group(host, "coop-popup-report-endpoint") + ',' + + get_reporting_group(host, "coop-popup-report-only-endpoint") ) if redirect != None: response.status = 302 diff --git a/html/cross-origin-opener-policy/resources/report.py b/html/cross-origin-opener-policy/resources/report.py new file mode 100644 index 00000000000000..45518cde9c3e48 --- /dev/null +++ b/html/cross-origin-opener-policy/resources/report.py @@ -0,0 +1,42 @@ +import json + +def main(request, response): + response.headers.set('Access-Control-Allow-Origin', '*') + response.headers.set('Access-Control-Allow-Methods', 'OPTIONS, GET, POST') + response.headers.set('Access-Control-Allow-Headers', 'Content-Type') + response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + + # CORS preflight + if request.method == 'OPTIONS': + return '' + + uuidMap = { + 'coop-report-endpoint': '01234567-0123-0123-0123-000000000001', + 'coop-report-only-endpoint': '01234567-0123-0123-0123-000000000002', + 'coop-popup-report-endpoint': '01234567-0123-0123-0123-000000000003', + 'coop-popup-report-only-endpoint': '01234567-0123-0123-0123-000000000004', + 'coop-redirect-report-endpoint': '01234567-0123-0123-0123-000000000005', + 'coop-redirect-report-only-endpoint': '01234567-0123-0123-0123-000000000006' + } + key = 0; + if 'endpoint' in request.GET: + key = uuidMap[request.GET['endpoint']] + + if key == 0: + response.status = 400 + return 'invalid endpoint' + + path = '/'.join(request.url_parts.path.split('/')[:-1]) + '/' + if request.method == 'POST': + reports = request.server.stash.take(key, path) or [] + for report in json.loads(request.body): + reports.append(report) + request.server.stash.put(key, reports, path) + return "done"+key+request.body + + if request.method == 'GET': + response.headers.set('Content-Type', 'application/json') + return json.dumps(request.server.stash.take(key, path) or []) + + response.status = 400 + return 'invalid method' diff --git a/html/cross-origin-opener-policy/resources/reporting-common.js b/html/cross-origin-opener-policy/resources/reporting-common.js new file mode 100644 index 00000000000000..948ee2e3a9e72b --- /dev/null +++ b/html/cross-origin-opener-policy/resources/reporting-common.js @@ -0,0 +1,153 @@ +// Allows RegExps to be pretty printed. +Object.defineProperty(RegExp.prototype, "toJSON", { + value: RegExp.prototype.toString +}); + +function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); +} + +async function pollReports(endpoint) { + const res = await fetch(`resources/report.py?endpoint=${endpoint.name}`, {cache: 'no-store'}); + if (res.status !== 200) { + return; + } + for (const report of await res.json()) { + endpoint.reports.push(report); + } +} + +function isObjectAsExpected(report, expectedReport) { + if((report === undefined || report === null || expectedReport === undefined || expectedReport === null) + && report !== expectedReport ) + return false; + + if(expectedReport instanceof RegExp && typeof report === "string") { + return expectedReport.test(report); + } + + // Perform this check now, as RegExp and strings above have different typeof. + if(typeof report !== typeof expectedReport) + return false; + + if(typeof expectedReport === 'object') { + return Object.keys(expectedReport).every(key => { + return isObjectAsExpected(report[key], expectedReport[key]); + }); + } + + return report == expectedReport; +} + +async function check_for_expected_report(expectedReport) { + return new Promise( async (resolve, reject) => { + const polls = 10; + const waitTime = 200; + for(var i=0; i < polls; ++i) { + pollReports(expectedReport.endpoint); + if(expectedReport.endpoint.reports.some((report) => { return isObjectAsExpected(report, expectedReport.report); })) + resolve(); + await wait(waitTime); + } + reject("No report matched the expected report for endpoint: " + expectedReport.endpoint.name + + ", expected report: " + JSON.stringify(expectedReport.report) + + ", within available reports: " + JSON.stringify(expectedReport.endpoint.reports) + ); + }); +} + +async function check_for_unwanted_report(unwantedReport) { + return new Promise( async (resolve, reject) => { + const polls = 10; + const waitTime = 200; + for(var i=0; i < polls; ++i) { + pollReports(unwantedReport.endpoint); + unwantedReport.endpoint.reports.forEach((report) => { + if(isObjectAsExpected(report, unwantedReport.report)) { + reject("Report matched the unwanted report for endpoint: " + unwantedReport.endpoint.name + + ", unwanted report: " + JSON.stringify(unwantedReport.report) + + ", matched: " + JSON.stringify(report) + ); + } + }); + await wait(waitTime); + } + resolve(); + }); +} + +function replace_from_regex_or_string(str, match, value) { + if(str instanceof RegExp) { + return RegExp(str.source.replace(match, value)); + } + return str.replace(match, value); +} + +function replace_values_in_expectedReport(expectedReport, channelName) { + if(expectedReport.report.body !== undefined) { + if(expectedReport.report.body["document-uri"] !== undefined) { + expectedReport.report.body["document-uri"] = replace_from_regex_or_string(expectedReport.report.body["document-uri"], "CHANNEL_NAME", channelName); + } + if(expectedReport.report.body["navigation-uri"] !== undefined) { + expectedReport.report.body["navigation-uri"] = replace_from_regex_or_string(expectedReport.report.body["navigation-uri"], "CHANNEL_NAME", channelName); + } + } + if(expectedReport.report.url !== undefined) { + expectedReport.report.url = replace_from_regex_or_string(expectedReport.report.url, "CHANNEL_NAME", channelName); + } + return expectedReport; +} + +async function reporting_test(testFunction, channelName, expectedReports, unexpectedReports) { + await new Promise( async resolve => { + testFunction(resolve); + }); + expectedReports = Array.from(expectedReports, report => replace_values_in_expectedReport(report, channelName) ); + unexpectedReports = Array.from(unexpectedReports, report => replace_values_in_expectedReport(report, channelName) ); + await Promise.all( + Array.from(expectedReports, check_for_expected_report).concat( + Array.from(unexpectedReports, check_for_unwanted_report)) + ); +} + +function coop_coep_reporting_test(testName, host, coop, coep, hasOpener, expectedReports, unexpectedReports){ + const channelName = `${testName.replace(/[ ;"=]/g,"-")}_to_${host.name}_${coop.replace(/[ ;"=]/g,"-")}_${coep}`; + promise_test(async t=> { + await reporting_test( (resolve) => { + coop_coep_test(t, host, coop, coep, channelName, + hasOpener, undefined /* openerDOMAccess */, resolve); + }, channelName, expectedReports, unexpectedReports); + }, `coop reporting test ${channelName}`); +} + +function run_coop_reporting_test(testName, tests){ + tests.forEach( test => { + coop_coep_reporting_test(testName, test[0], test[1], test[2], test[3], test[4], test[5]); + }); +} + +const reportEndpoint = { + name: "coop-report-endpoint", + reports: [] +} +const reportOnlyEndpoint = { + name: "coop-report-only-endpoint", + reports: [] +} +const popupReportEndpoint = { + name: "coop-popup-report-endpoint", + reports: [] +} +const popupReportOnlyEndpoint = { + name: "coop-popup-report-only-endpoint", + reports: [] +} +const redirectReportEndpoint = { + name: "coop-redirect-report-endpoint", + reports: [] +} +const redirectReportOnlyEndpoint = { + name: "coop-redirect-report-only-endpoint", + reports: [] +} +