From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- .../resources/call-functionCalledByOpenee.html | 5 + .../cross-origin-opener-policy/resources/common.js | 86 ++++++++ .../resources/coop-coep.py | 84 ++++++++ .../resources/coop-same-origin-repeated.asis | 24 +++ .../resources/csp-sandbox.py | 29 +++ .../resources/fully-loaded.js | 10 + .../resources/iframe-test.js | 234 +++++++++++++++++++++ .../resources/popup-test.js | 99 +++++++++ .../resources/postback.html | 45 ++++ .../resources/postback.html.headers | 2 + .../resources/redirect.py | 5 + .../resources/resource-cleanup.html | 11 + .../resources/resource-popup.html | 21 ++ .../resources/universal-worker.js | 2 + 14 files changed, 657 insertions(+) create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/call-functionCalledByOpenee.html create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/common.js create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-coep.py create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-same-origin-repeated.asis create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/csp-sandbox.py create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/fully-loaded.js create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/iframe-test.js create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/popup-test.js create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html.headers create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/redirect.py create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-cleanup.html create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-popup.html create mode 100644 testing/web-platform/tests/html/cross-origin-opener-policy/resources/universal-worker.js (limited to 'testing/web-platform/tests/html/cross-origin-opener-policy/resources') diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/call-functionCalledByOpenee.html b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/call-functionCalledByOpenee.html new file mode 100644 index 0000000000..d0ff0b723e --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/call-functionCalledByOpenee.html @@ -0,0 +1,5 @@ + + + diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/common.js b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/common.js new file mode 100644 index 0000000000..a005cb8a20 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/common.js @@ -0,0 +1,86 @@ +// To use the functions below, be sure to include the following files in your +// test: +// - "/common/get-host-info.sub.js" to get the different origin values. + +const SAME_ORIGIN = {origin: get_host_info().HTTPS_ORIGIN, name: "SAME_ORIGIN"}; +const SAME_SITE = {origin: get_host_info().HTTPS_REMOTE_ORIGIN, name: "SAME_SITE"}; +const CROSS_ORIGIN = {origin: get_host_info().HTTPS_NOTSAMESITE_ORIGIN, name: "CROSS_ORIGIN"} + +function addScriptAndTriggerOnload(src, onload){ + return `script = document.createElement("script"); + script.src= "${src}" ; + script.onload = () => { + ${onload} + }; + document.head.append(script);` +} + +function verify_window(callback, w, hasOpener) { + // If there's no opener, the w must be closed: + assert_equals(w.closed, !hasOpener, 'w.closed'); + // Opener's access on w.length is possible only if hasOpener: + assert_equals(w.length, hasOpener? 1: 0, 'w.length'); + callback(); +} + +function validate_results(callback, test, w, channelName, hasOpener, openerDOMAccess, payload) { + assert_equals(payload.name, hasOpener ? channelName : "", 'name'); + assert_equals(payload.opener, hasOpener, 'opener'); + // TODO(zcorpan): add openerDOMAccess expectations to all tests + if (openerDOMAccess !== undefined) { + assert_equals(payload.openerDOMAccess, openerDOMAccess, 'openerDOMAccess'); + } + + // The window proxy in Chromium might still reflect the previous frame, + // until its unloaded. This delays the verification of w here. + if( !w.closed && w.length == 0) { + test.step_timeout( () => { + verify_window(callback, w, hasOpener); + }, 500); + } else { + verify_window(callback, w, hasOpener); + } +} + +// Note: This function is deprecated and should not be used by new tests. +// Instead, use `dispatcher_url_test()`. +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(callback, t, w, channelName, hasOpener, openerDOMAccess, payload); + }); + + const w = window.open(url, channelName); + + // Close the popup once the test is complete. + // The browsing context might be closed hence use the broadcast channel + // to trigger the closure. + t.add_cleanup(() => { + bc.postMessage("close"); + }); +} + +// Similar to `url_test()` above except that this uses a dispatcher instead of +// BroadcastChannel (useful in cases where the context we are testing in is a +// third-party iframe that doesn't share a partition with the top-level +// site). +async function dispatcher_url_test(t, url, responseToken, iframeToken, hasOpener, openerDOMAccess, callback) { + + const w = window.open(url, responseToken); + + // Close the popup once the test is complete. + // The browsing context might be closed hence we'll have the iframe trigger + // the closure by sending it a 'close' message. + t.add_cleanup(async () => { + await send(iframeToken, "close"); + }); + + var payload = await receive(responseToken); + payload = JSON.parse(payload); + validate_results(callback, t, w, responseToken, hasOpener, openerDOMAccess, payload); +} + diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-coep.py b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-coep.py new file mode 100644 index 0000000000..d8e3bf0d42 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-coep.py @@ -0,0 +1,84 @@ +import json + +def main(request, response): + requestData = request.GET + if request.method == u"POST": + requestData = request.POST + + coop = requestData.first(b"coop") + coopReportOnly = requestData.first(b"coop-report-only", None) + coep = requestData.first(b"coep") + coepReportOnly = requestData.first(b"coep-report-only", None) + redirect = requestData.first(b"redirect", None) + if coop != b"": + response.headers.set(b"Cross-Origin-Opener-Policy", coop) + if coopReportOnly is not None: + response.headers.set(b"Cross-Origin-Opener-Policy-Report-Only", coopReportOnly) + if coep != b"": + response.headers.set(b"Cross-Origin-Embedder-Policy", coep) + if coepReportOnly is not None: + response.headers.set(b"Cross-Origin-Embedder-Policy-Report-Only", coepReportOnly) + if b'cache' in requestData: + response.headers.set(b'Cache-Control', b'max-age=3600') + host = request.url_parts[1] + + if redirect != None: + response.status = 302 + response.headers.set(b"Location", redirect) + return + + # Collect relevant params to be visible to response JS + params = {} + for key in (b"navHistory", b"avoidBackAndForth", b"navigate", b"channel", b"responseToken", b"iframeToken"): + value = requestData.first(key, None) + params[key.decode()] = value and value.decode() + + response.content = b""" + + + + + + + +""" % json.dumps(params).encode("utf-8") diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-same-origin-repeated.asis b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-same-origin-repeated.asis new file mode 100644 index 0000000000..082478e159 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/coop-same-origin-repeated.asis @@ -0,0 +1,24 @@ +HTTP/1.1 200 OK +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Opener-Policy: same-origin +Server: BaseHTTP/0.3 Python/2.7.15+ +Date: Wed, 18 Dec 2019 00:47:08 GMT + + + + + + diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/csp-sandbox.py b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/csp-sandbox.py new file mode 100644 index 0000000000..6cf21aeccf --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/csp-sandbox.py @@ -0,0 +1,29 @@ +def main(request, response): + coop = request.GET.first(b"coop") + coep = request.GET.first(b"coep") + sandbox = request.GET.first(b"sandbox") + if coop != "": + response.headers.set(b"Cross-Origin-Opener-Policy", coop) + if coep != "": + response.headers.set(b"Cross-Origin-Embedder-Policy", coep) + response.headers.set(b"Content-Security-Policy", b"sandbox " + sandbox + b";") + + # Open a popup to coop-coep.py with the same parameters (except sandbox) + response.content = b""" + + + + + +""" diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/fully-loaded.js b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/fully-loaded.js new file mode 100644 index 0000000000..d40e00af43 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/fully-loaded.js @@ -0,0 +1,10 @@ +// Return a promise, which resolves when new navigations aren't considered +// client-side redirects anymore. +// +// Note: A long `setTimeout` is used, because client-side redirect is an +// heuristic and isn't clearly specified. +function fullyLoaded() { + return new Promise((resolve, reject) => { + addEventListener('load', () => setTimeout(resolve, 2000)) + }); +} diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/iframe-test.js b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/iframe-test.js new file mode 100644 index 0000000000..a18688caf7 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/iframe-test.js @@ -0,0 +1,234 @@ +// To use the functions below, be sure to include the following files in your +// test: +// - "/common/get-host-info.sub.js" to get the different origin values. +// - "common.js" to have the origins easily available. +// - "/common/dispatcher/dispatcher.js" for cross-origin messaging. +// - "/common/utils.js" for token(). + +function getBaseExecutorPath(origin) { + return origin + '/common/dispatcher/executor.html'; +} + +function getHeadersPipe(headers) { + const coop_header = headers.coop ? + `|header(Cross-Origin-Opener-Policy,${encodeURIComponent(headers.coop)})` : ''; + const coep_header = headers.coep ? + `|header(Cross-Origin-Embedder-Policy,${encodeURIComponent(headers.coep)})` : ''; + return coop_header + coep_header; +} + +function getExecutorPath(uuid, origin, headers) { + return getBaseExecutorPath(origin) + + `?uuid=${uuid}` + + `&pipe=${getHeadersPipe(headers)}`; +} + +function evaluate(target_token, script) { + const reply_token = token(); + send(target_token, `send('${reply_token}', ${script});`); + return receive(reply_token); +} + +// Return true if an opened iframe can access |property| on a stored +// window.popup object without throwing an error. +function iframeCanAccessProperty(iframe_token, property) { + const reply_token = token(); + send(iframe_token, + `try { + const unused = window.popup['${property}']; + send('${reply_token}', 'true') + } catch (errors) { + send('${reply_token}', 'false') + }`); + return receive(reply_token); +} + +// Returns the script necessary to open a popup, given the method in +// `popup_via`. Supported methods are 'window_open' that leverages +// window.open(), 'anchor' that creates an HTML element and clicks on it, +// and 'form' that creates a form and submits it. +function popupOpeningScript(popup_via, popup_url, popup_origin, headers, + popup_token) { + if (popup_via === 'window_open') + return `window.popup = window.open('${popup_url}', '${popup_token}');`; + + if (popup_via === 'anchor') { + return ` + const anchor = document.createElement('a'); + anchor.href = '${popup_url}'; + anchor.rel = "opener"; + anchor.target = '${popup_token}'; + anchor.innerText = "anchor"; + document.body.appendChild(anchor); + anchor.click(); + `; + } + + if (popup_via === "form") { + return ` + const form = document.createElement("form"); + form.action = '${getBaseExecutorPath(popup_origin.origin)}'; + form.target = '${popup_token}'; + form.method = 'GET'; + const add_param = (name, value) => { + const input = document.createElement("input"); + input.name = name; + input.value = value; + form.appendChild(input); + }; + add_param("uuid", "${popup_token}"); + add_param("pipe", "${getHeadersPipe(headers)}"); + document.body.appendChild(form); + form.submit(); + `; + } + + assert_not_reached('Unrecognized popup opening method.'); +} + + +// Verifies that a popup with origin `popup_origin` and headers `headers` has +// the expected `opener_state` after being opened from an iframe with origin +// `iframe_origin`. +function iframe_test(description, iframe_origin, popup_origin, headers, + expected_opener_state) { + for (const popup_via of ['window_open', 'anchor','form']) { + promise_test(async t => { + const iframe_token = token(); + const popup_token = token(); + const reply_token = token(); + + const frame = document.createElement("iframe"); + const iframe_url = getExecutorPath( + iframe_token, + iframe_origin.origin, + {}); + + frame.src = iframe_url; + document.body.append(frame); + + send(iframe_token, `send('${reply_token}', 'Iframe loaded');`); + assert_equals(await receive(reply_token), 'Iframe loaded'); + + const popup_url = getExecutorPath( + popup_token, + popup_origin.origin, + headers); + + // We open popup and then ping it, it will respond after loading. + send(iframe_token, popupOpeningScript(popup_via, popup_url, popup_origin, + headers, popup_token)); + send(popup_token, `send('${reply_token}', 'Popup loaded');`); + assert_equals(await receive(reply_token), 'Popup loaded'); + + // Make sure the popup and the iframe are removed once the test has run, + // keeping a clean state. + add_completion_callback(() => { + frame.remove(); + send(popup_token, `close()`); + }); + + // Give some time for things to settle across processes etc. before + // proceeding with verifications. + await new Promise(resolve => { t.step_timeout(resolve, 500); }); + + // Verify that the opener is in the state we expect it to be in. + switch (expected_opener_state) { + case 'preserved': { + assert_equals( + await evaluate(popup_token, 'opener != null'), "true", + 'Popup has an opener?'); + assert_equals( + await evaluate(popup_token, `name === '${popup_token}'`), "true", + 'Popup has a name?'); + + // When the popup was created using window.open, we've kept a handle + // and we can do extra verifications. + if (popup_via === 'window_open') { + assert_equals( + await evaluate(iframe_token, 'popup != null'), "true", + 'Popup handle is non-null in iframe?'); + assert_equals( + await evaluate(iframe_token, 'popup.closed'), "false", + 'Popup appears closed from iframe?'); + assert_equals( + await iframeCanAccessProperty(iframe_token, "document"), + popup_origin === iframe_origin ? "true" : "false", + 'Iframe has dom access to the popup?'); + assert_equals( + await iframeCanAccessProperty(iframe_token, "frames"), "true", + 'Iframe has cross origin access to the popup?'); + } + break; + } + case 'restricted': { + assert_equals( + await evaluate(popup_token, 'opener != null'), "true", + 'Popup has an opener?'); + assert_equals( + await evaluate(popup_token, `name === '${popup_token}'`), "true", + 'Popup has a name?'); + + // When the popup was created using window.open, we've kept a handle + // and we can do extra verifications. + if (popup_via === 'window_open') { + assert_equals( + await evaluate(iframe_token, 'popup != null'), "true", + 'Popup handle is non-null in iframe?'); + assert_equals( + await evaluate(iframe_token, 'popup.closed'), "false", + 'Popup appears closed from iframe?'); + assert_equals( + await iframeCanAccessProperty(iframe_token, "document"), "false", + 'Iframe has dom access to the popup?'); + assert_equals( + await iframeCanAccessProperty(iframe_token, "frames"), "false", + 'Iframe has cross origin access to the popup?'); + assert_equals( + await iframeCanAccessProperty(iframe_token, "closed"), "true", + 'Iframe has limited cross origin access to the popup?'); + } + break; + } + case 'severed': { + assert_equals(await evaluate(popup_token, 'opener != null'), "false", + 'Popup has an opener?'); + assert_equals( + await evaluate(popup_token, `name === '${popup_token}'`), "false", + 'Popup has a name?'); + + // When the popup was created using window.open, we've kept a handle + // and we can do extra verifications. + if (popup_via === 'window_open') { + assert_equals( + await evaluate(iframe_token, 'popup != null'), "true", + 'Popup handle is non-null in iframe?'); + assert_equals( + await evaluate(iframe_token, 'popup.closed'), "true", + 'Popup appears closed from iframe?'); + } + break; + } + case 'noopener': { + assert_equals(await evaluate(popup_token, 'opener != null'), "false", + 'Popup has an opener?'); + assert_equals( + await evaluate(popup_token, `name === '${popup_token}'`), "false", + 'Popup has a name?'); + + // When the popup was created using window.open, we've kept a handle + // and we can do extra verifications. + if (popup_via === 'window_open') { + assert_equals( + await evaluate(iframe_token, 'popup == null'), "true", + 'Popup handle is null in iframe?'); + } + break; + } + default: + assert_not_reached('Unrecognized opener state: ' + + expected_opener_state); + } + }, `${description} with ${popup_via}`); + } +} diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/popup-test.js b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/popup-test.js new file mode 100644 index 0000000000..c2717bb135 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/popup-test.js @@ -0,0 +1,99 @@ +// To use the functions below, be sure to include the following files in your +// test: +// - "/common/get-host-info.sub.js" to get the different origin values. +// - "common.js" to have the origins easily available. +// - "/common/dispatcher/dispatcher.js" for cross-origin messaging. +// - "/common/utils.js" for token(). + +function getExecutorPath(uuid, origin, headers) { + const executor_path = '/common/dispatcher/executor.html?'; + const coop_header = headers.coop ? + `|header(Cross-Origin-Opener-Policy,${encodeURIComponent(headers.coop)})` : ''; + const coep_header = headers.coep ? + `|header(Cross-Origin-Embedder-Policy,${encodeURIComponent(headers.coep)})` : ''; + return origin + + executor_path + + `uuid=${uuid}` + + '&pipe=' + coop_header + coep_header; +} + +function getPopupHasOpener(popup_token) { + const reply_token = token(); + send(popup_token, `send('${reply_token}', window.opener != null);`); + return receive(reply_token); +} + +// Return true if |object|.|property| can be called without throwing an error. +function canAccessProperty(object, property) { + try { + const unused = object[property]; + return true; + } catch (errors) { + return false; + } +} + +// Verifies that a popup with origin `origin` and headers `headers` has +// the expected `opener_state` after being opened. +async function popup_test(description, origin, headers, expected_opener_state) { + promise_test(async t => { + const popup_token = token(); + const reply_token = token(); + + const popup_url = getExecutorPath( + popup_token, + origin.origin, + headers); + + // We open popup and then ping it, it will respond after loading. + const popup = window.open(popup_url); + send(popup_token, `send('${reply_token}', 'Popup loaded');`); + assert_equals(await receive(reply_token), 'Popup loaded'); + + // Make sure the popup will be closed once the test has run, keeping a clean + // state. + t.add_cleanup(() => { + send(popup_token, `close()`); + }); + + // Give some time for things to settle across processes etc. before + // proceeding with verifications. + await new Promise(resolve => { t.step_timeout(resolve, 500); }); + + // Verify that the opener is in the state we expect it to be in. + switch (expected_opener_state) { + case 'preserved': { + assert_false(popup.closed, 'Popup is closed from opener?'); + assert_true(await getPopupHasOpener(popup_token) === "true", + 'Popup has nulled opener?'); + assert_equals(canAccessProperty(popup, "document"), + origin === SAME_ORIGIN, + 'Main page has dom access to the popup?'); + assert_true(canAccessProperty(popup, "frames"), + 'Main page has cross origin access to the popup?'); + break; + } + case 'restricted': { + assert_false(popup.closed, 'Popup is closed from opener?'); + assert_true(await getPopupHasOpener(popup_token) === "true", + 'Popup has nulled opener?'); + assert_false(canAccessProperty(popup, "document"), + 'Main page has dom access to the popup?'); + assert_false(canAccessProperty(popup, "frames"), + 'Main page has cross origin access to the popup?'); + assert_true(canAccessProperty(popup, "closed"), + 'Main page has limited cross origin access to the popup?'); + break; + } + case 'severed': { + assert_true(popup.closed, 'Popup is closed from opener?'); + assert_false(await getPopupHasOpener(popup_token) === "true", + 'Popup has nulled opener?'); + break; + } + default: + assert_unreached(true, "Unrecognized opener relationship: " + expected_opener_state); + } + }, description); +} + diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html new file mode 100644 index 0000000000..35b3be5c93 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html @@ -0,0 +1,45 @@ + + + + diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html.headers b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html.headers new file mode 100644 index 0000000000..4e798cd9f5 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/postback.html.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: cross-origin diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/redirect.py b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/redirect.py new file mode 100644 index 0000000000..88dbd60fae --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/redirect.py @@ -0,0 +1,5 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + response.status = 302 + response.headers.set(b"Location", isomorphic_encode(request.url[request.url.find(u'?')+1:])) diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-cleanup.html b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-cleanup.html new file mode 100644 index 0000000000..3ae5587c7d --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-cleanup.html @@ -0,0 +1,11 @@ + + + +Redirect destination for non-HTML documents to close themselves + diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-popup.html b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-popup.html new file mode 100644 index 0000000000..2957e35f59 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/resource-popup.html @@ -0,0 +1,21 @@ + + + + + diff --git a/testing/web-platform/tests/html/cross-origin-opener-policy/resources/universal-worker.js b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/universal-worker.js new file mode 100644 index 0000000000..2441679372 --- /dev/null +++ b/testing/web-platform/tests/html/cross-origin-opener-policy/resources/universal-worker.js @@ -0,0 +1,2 @@ +onmessage = message => eval(message.data); +onfetch = event => fetchHandler(event); -- cgit v1.2.3