// Maps protocol (without the trailing colon) and address space to port. // // TODO(crbug.com/418737577): change keys to be consistent with new address // space names. const SERVER_PORTS = { "http": { "loopback": {{ports[http][0]}}, "other-loopback": {{ports[http][1]}}, "local": {{ports[http-local][0]}}, "public": {{ports[http-public][0]}}, }, "https": { "loopback": {{ports[https][0]}}, "other-loopback": {{ports[https][1]}}, "local": {{ports[https-local][0]}}, "public": {{ports[https-public][0]}}, }, "ws": { "loopback": {{ports[ws][0]}}, }, "wss": { "loopback": {{ports[wss][0]}}, }, }; // A `Server` is a web server accessible by tests. It has the following shape: // // { // addressSpace: the IP address space of the server ("loopback", "local" or // "public"), // name: a human-readable name for the server, // port: the port on which the server listens for connections, // protocol: the protocol (including trailing colon) spoken by the server, // } // // Constants below define the available servers, which can also be accessed // programmatically with `get()`. class Server { // Maps the given `protocol` (without a trailing colon) and `addressSpace` to // a server. Returns null if no such server exists. static get(protocol, addressSpace) { const ports = SERVER_PORTS[protocol]; if (ports === undefined) { return null; } const port = ports[addressSpace]; if (port === undefined) { return null; } return { addressSpace, name: `${protocol}-${addressSpace}`, port, protocol: protocol + ':', }; } static HTTP_LOOPBACK = Server.get("http", "loopback"); static OTHER_HTTP_LOOPBACK = Server.get("http", "other-loopback"); static HTTP_LOCAL = Server.get("http", "local"); static HTTP_PUBLIC = Server.get("http", "public"); static HTTPS_LOOPBACK = Server.get("https", "loopback"); static OTHER_HTTPS_LOOPBACK = Server.get("https", "other-loopback"); static HTTPS_LOCAL = Server.get("https", "local"); static HTTPS_PUBLIC = Server.get("https", "public"); static WS_LOOPBACK = Server.get("ws", "loopback"); static WSS_LOOPBACK = Server.get("wss", "loopback"); }; // Resolves a URL relative to the current location, returning an absolute URL. // // `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example". // `options`, if defined, should have the following shape: // // { // // Optional. Overrides the protocol of the returned URL. // protocol, // // // Optional. Overrides the port of the returned URL. // port, // // // Extra headers. // headers, // // // Extra search params. // searchParams, // } // function resolveUrl(url, options) { const result = new URL(url, window.location); if (options === undefined) { return result; } const { port, protocol, headers, searchParams } = options; if (port !== undefined) { result.port = port; } if (protocol !== undefined) { result.protocol = protocol; } if (headers !== undefined) { const pipes = []; for (key in headers) { pipes.push(`header(${key},${headers[key]})`); } result.searchParams.append("pipe", pipes.join("|")); } if (searchParams !== undefined) { for (key in searchParams) { result.searchParams.append(key, searchParams[key]); } } return result; } // Computes options to pass to `resolveUrl()` for a source document's URL. // // `server` identifies the server from which to load the document. // `treatAsPublic`, if set to true, specifies that the source document should // be artificially placed in the `public` address space using CSP. function sourceResolveOptions({ server, treatAsPublic }) { const options = {...server}; if (treatAsPublic) { options.headers = { "Content-Security-Policy": "treat-as-public-address" }; } return options; } // Computes the URL of a target handler configured with the given options. // // `server` identifies the server from which to load the resource. // `behavior` specifies the behavior of the target server. It may contain: // - `response`: The result of calling one of `ResponseBehavior`'s methods. // - `redirect`: A URL to which the target should redirect GET requests. function resolveTargetUrl({ server, behavior }) { if (server === undefined) { throw new Error("no server specified."); } const options = {...server}; if (behavior) { const { response, redirect } = behavior; options.searchParams = { ...response, }; if (redirect !== undefined) { options.searchParams.redirect = redirect; } } return resolveUrl("target.py", options); } // Methods generate behavior specifications for how `resources/target.py` // should behave upon receiving a regular (non-preflight) request. const ResponseBehavior = { // The response should succeed without CORS headers. default: () => ({}), // The response should succeed with CORS headers. allowCrossOrigin: () => ({ "final-headers": "cors" }), }; const FetchTestResult = { SUCCESS: { ok: true, body: "success", }, OPAQUE: { ok: false, type: "opaque", body: "", }, FAILURE: { error: "TypeError: Failed to fetch", }, }; // Helper function for checking results from fetch tests. function checkTestResult(actual, expected) { assert_equals(actual.error, expected.error, "error mismatch"); assert_equals(actual.ok, expected.ok, "response ok mismatch"); assert_equals(actual.body, expected.body, "response body mismatch"); if (expected.type !== undefined) { assert_equals(type, expected.type, "response type mismatch"); } } // Registers an event listener that will resolve this promise when this // window receives a message posted to it. function futureMessage(options) { return new Promise(resolve => { window.addEventListener('message', (e) => { resolve(e.data); }); }); }; const NavigationTestResult = { SUCCESS: 'loaded', FAILURE: 'timeout', }; async function iframeTest( t, {source, target, expected, permission = 'denied'}) { const targetUrl = resolveUrl('resources/openee.html', sourceResolveOptions(target)); const sourceUrl = resolveUrl('resources/iframer.html', sourceResolveOptions(source)); sourceUrl.searchParams.set('permission', permission); sourceUrl.searchParams.set('url', targetUrl); const popup = window.open(sourceUrl); t.add_cleanup(() => popup.close()); // The child frame posts a message iff it loads successfully. // There exists no interoperable way to check whether an iframe failed to // load, so we use a timeout. // See: https://github.com/whatwg/html/issues/125 const result = await Promise.race([ futureMessage().then((data) => data.message), new Promise((resolve) => { t.step_timeout(() => resolve('timeout'), 2000 /* ms */); }), ]); assert_equals(result, expected); } async function navigateTest(t, {source, target, expected}) { const targetUrl = resolveUrl('resources/openee.html', sourceResolveOptions(target)); const sourceUrl = resolveUrl('resources/navigate.html', sourceResolveOptions(source)); sourceUrl.searchParams.set('url', targetUrl); const popup = window.open(sourceUrl); t.add_cleanup(() => popup.close()); const result = await Promise.race([ futureMessage().then((data) => data.message), new Promise((resolve) => { t.step_timeout(() => resolve('timeout'), 2000 /* ms */); }), ]); assert_equals(result, expected); }