diff options
Diffstat (limited to 'testing/web-platform/tests/fetch/connection-pool')
8 files changed, 567 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/connection-pool/network-partition-key.html b/testing/web-platform/tests/fetch/connection-pool/network-partition-key.html new file mode 100644 index 0000000000..60a784cd84 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/network-partition-key.html @@ -0,0 +1,264 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Connection partitioning by site</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/utils.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</head> +<body> +<!-- Used to open about:blank tabs from opaque origins --> +<iframe id="iframe0" sandbox="allow-popups allow-scripts allow-popups-to-escape-sandbox"></iframe> +<iframe id="iframe1" sandbox="allow-popups allow-scripts allow-popups-to-escape-sandbox"></iframe> +<script> +const host = get_host_info(); + +// These two origins must correspond to different sites for this test to pass. +const POPUP_ORIGINS = [ + host.ORIGIN, + host.HTTP_NOTSAMESITE_ORIGIN +]; + +// This origin should ideally correspond to a different site from the two above, but the +// tests will still pass if it matches the site of one of the other two origins. +const OTHER_ORIGIN = host.REMOTE_ORIGIN; + +// Except for the csp_sandbox and about:blanks, each test opens up two windows, one at +// POPUP_ORIGINS[0], one at POPUP_ORIGINS[1], and has them request subresources from +// subresource_origin. All requests (HTML, JS, and fetch requests) for each window go +// through network-partition-key.py and have a partition_id parameter, which is used +// to check if any request for one window uses the same socket as a request for the +// other window. +// +// Whenever requests from the two different popup windows use the same connection, the +// fetch requests all start returning 400 errors, but other requests will continue to +// succeed, to make for clearer errors. +// +// include_credentials indicates whether the fetch requests use credentials or not, +// which is interesting as uncredentialed sockets have separate connection pools. +const tests = [ + { + name: 'With credentials', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: true, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Without credentials', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: false, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Cross-site resources with credentials', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Cross-site resources without credentials', + subresource_origin: OTHER_ORIGIN, + include_credentials: false, + popup_params: [ + {type: 'main_frame'}, + {type: 'main_frame'} + ] + }, + { + name: 'Iframes', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + { + type: 'iframe', + iframe_origin: OTHER_ORIGIN + }, + { + type: 'iframe', + iframe_origin: OTHER_ORIGIN + } + ] + }, + { + name: 'Workers', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: true, + popup_params: [ + {type: 'worker'}, + {type: 'worker'} + ] + }, + { + name: 'Workers with cross-site resources', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + {type: 'worker'}, + {type: 'worker'} + ] + }, + { + name: 'CSP sandbox', + subresource_origin: POPUP_ORIGINS[0], + include_credentials: true, + popup_params: [ + {type: 'csp_sandbox'}, + {type: 'csp_sandbox'} + ] + }, + { + name: 'about:blank from opaque origin iframe', + subresource_origin: OTHER_ORIGIN, + include_credentials: true, + popup_params: [ + {type: 'opaque_about_blank'}, + {type: 'opaque_about_blank'} + ] + }, +]; + +const BASE_PATH = window.location.pathname.replace(/\/[^\/]*$/, '/'); + +function create_script_url(origin, uuid, partition_id, dispatch) { + return `${origin}${BASE_PATH}resources/network-partition-key.py?uuid=${uuid}&partition_id=${partition_id}&dispatch=${dispatch}` +} + +function run_test(test) { + var uuid = token(); + + // Used to track the opened popup windows, so they can be closed at the end of the test. + // They could be closed immediately after use, but safest to keep them open, as browsers + // could use closing a window as a hint to close idle sockets that the window used. + var popup_windows = []; + + // Creates a popup window at |url| and waits for a test result. Returns a promise. + function create_popup_and_wait_for_result(url) { + return new Promise(function(resolve, reject) { + popup_windows.push(window.open(url)); + // Listen for the result + function message_listener(event) { + if (event.data.result === 'success') { + resolve(); + } else if (event.data.result === 'error') { + reject(event.data.details); + } else { + reject('Unexpected message.'); + } + } + window.addEventListener('message', message_listener, {once: 'true'}); + }); + } + + // Navigates iframe to url and waits for a test result. Returns a promise. + function navigate_iframe_and_wait_for_result(iframe, url) { + return new Promise(function(resolve, reject) { + iframe.src = url; + // Listen for the result + function message_listener(event) { + if (event.data.result === 'success') { + resolve(); + } else if (event.data.result === 'error') { + reject(event.data.details); + } else { + reject('Unexpected message.'); + } + } + window.addEventListener('message', message_listener, {once: 'true'}); + }); + } + + function make_test_function(test, index) { + var popup_params = test.popup_params[index]; + return function() { + var popup_path; + var additional_url_params = ''; + var origin = POPUP_ORIGINS[index]; + var partition_id = POPUP_ORIGINS[index]; + if (popup_params.type == 'main_frame') { + popup_path = 'resources/network-partition-checker.html'; + } else if (popup_params.type == 'iframe') { + popup_path = 'resources/network-partition-iframe-checker.html'; + additional_url_params = `&other_origin=${popup_params.iframe_origin}`; + } else if (popup_params.type == 'worker') { + popup_path = 'resources/network-partition-worker-checker.html'; + // The origin of the dedicated worker must mutch the page that loads it. + additional_url_params = `&other_origin=${POPUP_ORIGINS[index]}`; + } else if (popup_params.type == 'csp_sandbox') { + // For the Content-Security-Policy sandbox test, all requests are from the same origin, but + // the origin should be treated as an opaque origin, so sockets should not be reused. + origin = test.subresource_origin; + partition_id = index; + popup_path = 'resources/network-partition-checker.html'; + // Don't check partition of root document, since the document isn't sandboxed until the + // root document is fetched. + additional_url_params = '&sandbox=true&nocheck_partition=true' + } else if (popup_params.type=='opaque_about_blank') { + popup_path = 'resources/network-partition-about-blank-checker.html'; + } else if (popup_params.type == 'iframe') { + throw 'Unrecognized popup_params.type.'; + } + var url = create_script_url(origin, uuid, partition_id, 'fetch_file'); + url += `&subresource_origin=${test.subresource_origin}` + url += `&include_credentials=${test.include_credentials}` + url += `&path=${BASE_PATH.substring(1)}${popup_path}`; + url += additional_url_params; + + if (popup_params.type=='opaque_about_blank') { + return navigate_iframe_and_wait_for_result(iframe = document.getElementById('iframe' + index), url); + } + + return create_popup_and_wait_for_result(url); + } + } + + // Takes a Promise, and cleans up state when the promise has completed, successfully or not, re-throwing + // any exception from the passed in Promise. + async function clean_up_when_done(promise) { + var error; + try { + await promise; + } catch (e) { + error = e; + } + + popup_windows.map(function (win) { win.close(); }); + + try { + var cleanup_url = create_script_url(host.ORIGIN, uuid, host.ORIGIN, 'clean_up'); + var response = await fetch(cleanup_url, {credentials: 'omit', mode: 'cors'}); + assert_equals(await response.text(), 'cleanup complete', `Sever state cleanup failed`); + } catch (e) { + // Prefer error from the passed in Promise over errors from the fetch request to clean up server state. + error = error || e; + } + if (error) + throw error; + } + + return clean_up_when_done( + make_test_function(test, 0)() + .then(make_test_function(test, 1))); +} + +tests.forEach(function (test) { + promise_test( + function() { return run_test(test); }, + test.name); +}) + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html new file mode 100644 index 0000000000..7a8b613237 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>about:blank Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> +</head> +<body> +<script> + async function fetch_and_reply() { + // Load about:blank in a new tab, and inject the network partition checking code into it. + var win; + try { + win = window.open(); + var url = 'SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-checker.html&sandbox=true'; + var response = await fetch(url, {credentials: 'omit', mode: 'cors'}); + win.document.write(await response.text()); + } catch (e) { + win.close(); + window.parent.postMessage({result: 'error', details: e.message}, '*'); + return; + } + + // Listen for first message from the new window and pass it back to the parent. + function message_listener(event) { + window.parent.postMessage(event.data, '*'); + win.close(); + } + window.addEventListener('message', message_listener, {once: true}); + } + fetch_and_reply(); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-checker.html new file mode 100644 index 0000000000..b058f61124 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-checker.html @@ -0,0 +1,30 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script> +</head> +<body> +<script> + async function fetch_and_reply() { + // If this is a top level window, report to the opener. Otherwise, this is an iframe, + // so report to the parent. + var report_to = window.opener; + if (!report_to) + report_to = window.parent; + try { + await check_partition_ids(); + report_to.postMessage({result: 'success'}, '*'); + } catch (e) { + report_to.postMessage({result: 'error', details: e.message}, '*'); + } + } + fetch_and_reply(); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html new file mode 100644 index 0000000000..f76ed18447 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html @@ -0,0 +1,22 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Iframe Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script> +</head> +<body> +<script> + // Listen for first message from the iframe, and pass it back to the opener. + function message_listener(event) { + window.opener.postMessage(event.data, '*'); + } + window.addEventListener('message', message_listener, {once: 'true'}); +</script> +<iframe src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-checker.html"></iframe> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.js b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.js new file mode 100644 index 0000000000..bd66109380 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.js @@ -0,0 +1,47 @@ +// Runs multiple fetches that validate connections see only a single partition_id. +// Requests are run in parallel so that they use multiple connections to maximize the +// chance of exercising all matching connections in the connection pool. Only returns +// once all requests have completed to make cleaning up server state non-racy. +function check_partition_ids(location) { + const NUM_FETCHES = 20; + + var base_url = 'SUBRESOURCE_PREFIX:&dispatch=check_partition'; + + // Not a perfect parse of the query string, but good enough for this test. + var include_credentials = base_url.search('include_credentials=true') != -1; + var exclude_credentials = base_url.search('include_credentials=false') != -1; + if (include_credentials != !exclude_credentials) + throw new Exception('Credentials mode not specified'); + + + // Run NUM_FETCHES in parallel. + var fetches = []; + for (i = 0; i < NUM_FETCHES; ++i) { + var fetch_params = { + credentials: 'omit', + mode: 'cors', + headers: { + 'Header-To-Force-CORS': 'cors' + }, + }; + + // Use a unique URL for each request, in case the caching layer serializes multiple + // requests for the same URL. + var url = `${base_url}&${token()}`; + + fetches.push(fetch(url, fetch_params).then( + function (response) { + return response.text().then(function(text) { + assert_equals(text, 'ok', `Socket unexpectedly reused`); + }); + })); + } + + // Wait for all promises to complete. + return Promise.allSettled(fetches).then(function (results) { + results.forEach(function (result) { + if (result.status != 'fulfilled') + throw result.reason; + }); + }); +} diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.py b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.py new file mode 100644 index 0000000000..32fe4999b7 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-key.py @@ -0,0 +1,130 @@ +import mimetypes +import os + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +# Test server that tracks the last partition_id was used with each connection for each uuid, and +# lets consumers query if multiple different partition_ids have been been used for any socket. +# +# Server assumes that ports aren't reused, so a client address and a server port uniquely identify +# a connection. If that constraint is ever violated, the test will be flaky. No sockets being +# closed for the duration of the test is sufficient to ensure that, though even if sockets are +# closed, the OS should generally prefer to use new ports for new connections, if any are +# available. +def main(request, response): + response.headers.set(b"Cache-Control", b"no-store") + dispatch = request.GET.first(b"dispatch", None) + uuid = request.GET.first(b"uuid", None) + partition_id = request.GET.first(b"partition_id", None) + + if not uuid or not dispatch or not partition_id: + return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") + + # Unless nocheck_partition is true, check partition_id against server_state, and update server_state. + stash = request.server.stash + test_failed = False + request_count = 0; + connection_count = 0; + if request.GET.first(b"nocheck_partition", None) != b"True": + # Need to grab the lock to access the Stash, since requests are made in parallel. + with stash.lock: + # Don't use server hostname here, since H2 allows multiple hosts to reuse a connection. + # Server IP is not currently available, unfortunately. + address_key = isomorphic_encode(str(request.client_address) + u"|" + str(request.url_parts.port)) + server_state = stash.take(uuid) or {b"test_failed": False, + b"request_count": 0, b"connection_count": 0} + request_count = server_state[b"request_count"] + request_count += 1 + server_state[b"request_count"] = request_count + if address_key in server_state: + if server_state[address_key] != partition_id: + server_state[b"test_failed"] = True + else: + connection_count = server_state[b"connection_count"] + connection_count += 1 + server_state[b"connection_count"] = connection_count + server_state[address_key] = partition_id + test_failed = server_state[b"test_failed"] + stash.put(uuid, server_state) + + origin = request.headers.get(b"Origin") + if origin: + response.headers.set(b"Access-Control-Allow-Origin", origin) + response.headers.set(b"Access-Control-Allow-Credentials", b"true") + + if request.method == u"OPTIONS": + return handle_preflight(request, response) + + if dispatch == b"fetch_file": + return handle_fetch_file(request, response, partition_id, uuid) + + if dispatch == b"check_partition": + status = request.GET.first(b"status", 200) + if test_failed: + return simple_response(request, response, status, b"OK", b"Multiple partition IDs used on a socket") + body = b"ok" + if request.GET.first(b"addcounter", False): + body += (". Request was sent " + str(request_count) + " times. " + + str(connection_count) + " connections were created.").encode('utf-8') + return simple_response(request, response, status, b"OK", body) + + if dispatch == b"clean_up": + stash.take(uuid) + if test_failed: + return simple_response(request, response, 200, b"OK", b"Test failed, but cleanup completed.") + return simple_response(request, response, 200, b"OK", b"cleanup complete") + + return simple_response(request, response, 404, b"Not Found", b"Unrecognized dispatch parameter: " + dispatch) + +def handle_preflight(request, response): + response.status = (200, b"OK") + response.headers.set(b"Access-Control-Allow-Methods", b"GET") + response.headers.set(b"Access-Control-Allow-Headers", b"header-to-force-cors") + response.headers.set(b"Access-Control-Max-Age", b"86400") + return b"Preflight request" + +def simple_response(request, response, status_code, status_message, body, content_type=b"text/plain"): + response.status = (status_code, status_message) + response.headers.set(b"Content-Type", content_type) + return body + +def handle_fetch_file(request, response, partition_id, uuid): + subresource_origin = request.GET.first(b"subresource_origin", None) + rel_path = request.GET.first(b"path", None) + + # This needs to be passed on to subresources so they all have access to it. + include_credentials = request.GET.first(b"include_credentials", None) + if not subresource_origin or not rel_path or not include_credentials: + return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") + + cur_path = os.path.realpath(isomorphic_decode(__file__)) + base_path = os.path.abspath(os.path.join(os.path.dirname(cur_path), os.pardir, os.pardir, os.pardir)) + path = os.path.abspath(os.path.join(base_path, isomorphic_decode(rel_path))) + + # Basic security check. + if not path.startswith(base_path): + return simple_response(request, response, 404, b"Not found", b"Invalid path") + + sandbox = request.GET.first(b"sandbox", None) + if sandbox == b"true": + response.headers.set(b"Content-Security-Policy", b"sandbox allow-scripts") + + file = open(path, mode="rb") + body = file.read() + file.close() + + subresource_path = b"/" + isomorphic_encode(os.path.relpath(isomorphic_decode(__file__), base_path)).replace(b'\\', b'/') + subresource_params = b"?partition_id=" + partition_id + b"&uuid=" + uuid + b"&subresource_origin=" + subresource_origin + b"&include_credentials=" + include_credentials + body = body.replace(b"SUBRESOURCE_PREFIX:", subresource_origin + subresource_path + subresource_params) + + other_origin = request.GET.first(b"other_origin", None) + if other_origin: + body = body.replace(b"OTHER_PREFIX:", other_origin + subresource_path + subresource_params) + + mimetypes.init() + mimetype_pair = mimetypes.guess_type(path) + mimetype = mimetype_pair[0] + + if mimetype == None or mimetype_pair[1] != None: + return simple_response(request, response, 500, b"Server Error", b"Unknown MIME type") + return simple_response(request, response, 200, b"OK", body, mimetype) diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker-checker.html b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker-checker.html new file mode 100644 index 0000000000..e6b7ea7673 --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker-checker.html @@ -0,0 +1,24 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Worker Network Partition Checker</title> + <meta name="help" href="https://fetch.spec.whatwg.org/#network-partition-keys"> + <meta name="timeout" content="normal"> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js"></script> + <script src="SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js"></script> +</head> +<body> +<script> + // Workers must be same origin as the page loading them, but it's simpler to reuse the + // OTHER_PREFIX mechanism in the Python code than to craft the URL in Javascript here. + var worker = new Worker('OTHER_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-worker.js'); + function message_listener(event) { + window.opener.postMessage(event.data, '*'); + worker.terminate(); + } + worker.addEventListener('message', message_listener); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker.js b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker.js new file mode 100644 index 0000000000..1745edfacb --- /dev/null +++ b/testing/web-platform/tests/fetch/connection-pool/resources/network-partition-worker.js @@ -0,0 +1,15 @@ +// This tests the partition key of fetches to subresouce_origin made by the worker and +// imported scripts from subresource_origin. +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js'); + +async function fetch_and_reply() { + try { + await check_partition_ids(); + self.postMessage({result: 'success'}); + } catch (e) { + self.postMessage({result: 'error', details: e.message}); + } +} +fetch_and_reply(); |