blob: e8425c240b5e5821bc16708044863f73bf5f4697 [file] [log] [blame] [edit]
/**
* Inserts an iframe usable for origin isolation testing, and returns a promise
* fulfilled when the iframe is loaded and its document.domain is set. The
* iframe will point to the send-origin-isolation-header.py file, on the
* designated host
* @param {string} host - The host used to calculate the iframe's src=""
* @param {string=} header - The value of the Origin-Isolation header that the
* iframe will set. Omit this to set no header.
* @returns {HTMLIFrameElement} The created iframe element
*/
export async function insertIframe(host, header) {
const iframe = document.createElement("iframe");
const navigatePromise = navigateIframe(iframe, host, header);
document.body.append(iframe);
await navigatePromise;
await setBothDocumentDomains(iframe.contentWindow);
return iframe;
}
/**
* Navigates an iframe to a page for origin isolation testing, similar to
* insertIframe but operating on an existing iframe.
* @param {HTMLIFrameElement} iframeEl - The <iframe> element to navigate
* @param {string} host - The host to calculate the iframe's new src=""
* @param {string=} header - The value of the Origin-Isolation header that the
* newly-navigated-to page will set. Omit this to set no header.
* @returns {Promise} a promise fulfilled when the load event fires, or rejected
* if the error event fires
*/
export function navigateIframe(iframeEl, host, header) {
const url = getIframeURL(host, header);
const waitPromise = waitForIframe(iframeEl, url);
iframeEl.src = url;
return waitPromise;
}
/**
* Returns a promise that is fulfilled when an iframe's load event fires, or
* rejected when its error event fires.
* @param {HTMLIFrameElement} iframeEl - The <iframe> element to wait on
* @param {string} destinationForErrorMessage - A string used in the promise
* rejection error message, if the error event fires
* @returns {Promise} a promise fulfilled when the load event fires, or rejected
* if the error event fires
*/
export function waitForIframe(iframeEl, destinationForErrorMessage) {
return new Promise((resolve, reject) => {
iframeEl.addEventListener("load", () => resolve());
iframeEl.addEventListener(
"error",
() => reject(new Error(`Could not navigate to ${destinationForErrorMessage}`))
);
});
}
/**
* Expands into a pair of promise_test() calls to ensure that two Windows are in
* the same agent cluster, by checking both that we can send a
* WebAssembly.Module, and that we can synchronously access the DOM.
* @param {Array} testFrames - An array of either the form [self, frameIndex] or
* [frameIndex1, frameIndex2], indicating the two Windows under test. E.g.
* [self, 0] or [0, 1].
* @param {string=} testLabelPrefix - A prefix used in the test names. This can
* be omitted if testSameAgentCluster is only used once in a test file.
*/
export function testSameAgentCluster(testFrames, testLabelPrefix) {
const prefix = testLabelPrefix === undefined ? "" : `${testLabelPrefix}: `;
if (testFrames[0] === self) {
// Between parent and a child at the index given by testFrames[1]
promise_test(async () => {
const frameWindow = frames[testFrames[1]];
const whatHappened = await sendWasmModule(frameWindow);
assert_equals(whatHappened, "WebAssembly.Module message received");
}, `${prefix}message event must occur`);
promise_test(async () => {
const frameWindow = frames[testFrames[1]];
// Must not throw
frameWindow.document;
// Must not throw
frameWindow.location.href;
}, `${prefix}setting document.domain must give sync access`);
} else {
// Between the two children at the index given by testFrames[0] and
// testFrames[1]
promise_test(async () => {
const whatHappened = await sendWasmModuleBetween(testFrames);
assert_equals(whatHappened, "WebAssembly.Module message received");
}, `${prefix}message event must occur`);
promise_test(async () => {
const whatHappened1 = await accessDocumentBetween(testFrames);
assert_equals(whatHappened1, "accessed document successfully");
const whatHappened2 = await accessLocationHrefBetween(testFrames);
assert_equals(whatHappened2, "accessed location.href successfully");
}, `${prefix}setting document.domain must give sync access`);
}
}
/**
* Expands into a pair of promise_test() calls to ensure that two Windows are in
* different agent clusters, by checking both that we cannot send a
* WebAssembly.Module, and that we cannot synchronously access the DOM.
* @param {Array} testFrames - An array of either the form [self, frameIndex] or
* [frameIndex1, frameIndex2], indicating the two Windows under test. E.g.
* [self, 0] or [0, 1].
* @param {string=} testLabelPrefix - A prefix used in the test names. This can
* be omitted if testDifferentAgentClusters is only used once in a test file.
*/
export function testDifferentAgentClusters(testFrames, testLabelPrefix) {
const prefix = testLabelPrefix === undefined ? "" : `${testLabelPrefix}: `;
if (testFrames[0] === self) {
// Between parent and a child at the index given by testFrames[1]
promise_test(async () => {
const frameWindow = frames[testFrames[1]];
const whatHappened = await sendWasmModule(frameWindow);
assert_equals(whatHappened, "messageerror");
}, `${prefix}messageerror event must occur`);
promise_test(async () => {
const frameWindow = frames[testFrames[1]];
assert_throws_dom("SecurityError", DOMException, () => {
frameWindow.document;
});
assert_throws_dom("SecurityError", DOMException, () => {
frameWindow.location.href;
});
}, `${prefix}setting document.domain must not give sync access`);
} else {
// Between the two children at the index given by testFrames[0] and
// testFrames[1]
promise_test(async () => {
const whatHappened = await sendWasmModuleBetween(testFrames);
assert_equals(whatHappened, "messageerror");
}, `${prefix}messageerror event must occur`);
promise_test(async () => {
const whatHappened1 = await accessDocumentBetween(testFrames);
assert_equals(whatHappened1, "SecurityError");
const whatHappened2 = await accessLocationHrefBetween(testFrames);
assert_equals(whatHappened2, "SecurityError");
}, `${prefix}setting document.domain must not give sync access`);
}
}
/**
* Creates a promise_test() to check the value of the originIsolationRestricted
* getter in the given testFrame.
* @param {Window|number} testFrame - Either self, or a frame index to test.
* @param {boolean} expected - The expected value for originIsolationRestricted.
* @param {string=} testLabelPrefix - A prefix used in the test names. This can
* be omitted if the function is only used once in a test file.
*/
export function testOriginIsolationRestricted(testFrame, expected, testLabelPrefix) {
const prefix = testLabelPrefix === undefined ? "" : `${testLabelPrefix}: `;
if (testFrame === self) {
// Need to use promise_test() even though it's sync because we use
// promise_setup() in many tests.
promise_test(async () => {
assert_equals(self.originIsolationRestricted, expected);
}, `${prefix}originIsolationRestricted must equal ${expected}`);
} else {
promise_test(async () => {
const frameWindow = frames[testFrame];
const result = await getOriginIsolationRestricted(frameWindow);
assert_equals(result, expected);
}, `${prefix}originIsolationRestricted must equal ${expected}`);
}
}
/**
* Sends a WebAssembly.Module instance to the given Window, and waits for it to
* send back a message indicating whether it got the module or got a
* messageerror event. (This relies on the given Window being derived from
* insertIframe or navigateIframe.)
* @param {Window} frameWindow - The destination Window
* @returns {Promise} A promise which will be fulfilled with either
* "WebAssembly.Module message received" or "messageerror"
*/
export async function sendWasmModule(frameWindow) {
// This function is coupled to ./send-origin-isolation-header.py, which ensures
// that sending such a message will result in a message back.
frameWindow.postMessage(await createWasmModule(), "*");
return waitForMessage(frameWindow);
}
/**
* Sets document.domain (to itself) for both the current Window and the given
* Window. The latter relies on the given Window being derived from insertIframe
* or navigateIframe.
* @param frameWindow - The other Window whose document.domain is to be set
* @returns {Promise} A promise which will be fulfilled after both
* document.domains are set
*/
export async function setBothDocumentDomains(frameWindow) {
// By setting both this page's document.domain and the iframe's
// document.domain to the same value, we ensure that they can synchronously
// access each other, unless they are origin-isolated.
// NOTE: document.domain being unset is different than it being set to its
// current value. It is a terrible API.
document.domain = document.domain;
// This function is coupled to ./send-origin-isolation-header.py, which ensures
// that sending such a message will result in a message back.
frameWindow.postMessage({ command: "set document.domain", newDocumentDomain: document.domain }, "*");
const whatHappened = await waitForMessage(frameWindow);
assert_equals(whatHappened, "document.domain is set");
}
async function getOriginIsolationRestricted(frameWindow) {
// This function is coupled to ./send-origin-isolation-header.py, which ensures
// that sending such a message will result in a message back.
frameWindow.postMessage({ command: "get originIsolationRestricted" }, "*");
return waitForMessage(frameWindow);
}
function getIframeURL(host, header) {
const url = new URL("send-origin-isolation-header.py", import.meta.url);
url.host = host;
if (header !== undefined) {
url.searchParams.set("header", header);
}
return url.href;
}
async function sendWasmModuleBetween(testFrames) {
const sourceFrame = frames[testFrames[0]];
const indexIntoParentFrameOfDestination = testFrames[1];
sourceFrame.postMessage({ command: "send WASM module", indexIntoParentFrameOfDestination }, "*");
return waitForMessage(sourceFrame);
}
async function accessDocumentBetween(testFrames) {
const sourceFrame = frames[testFrames[0]];
const indexIntoParentFrameOfDestination = testFrames[1];
sourceFrame.postMessage({ command: "access document", indexIntoParentFrameOfDestination }, "*");
return waitForMessage(sourceFrame);
}
async function accessLocationHrefBetween(testFrames) {
const sourceFrame = frames[testFrames[0]];
const indexIntoParentFrameOfDestination = testFrames[1];
sourceFrame.postMessage({ command: "access location.href", indexIntoParentFrameOfDestination }, "*");
return waitForMessage(sourceFrame);
}
function waitForMessage(expectedSource) {
return new Promise(resolve => {
const handler = e => {
if (e.source === expectedSource) {
resolve(e.data);
window.removeEventListener("message", handler);
}
};
window.addEventListener("message", handler);
});
}
// Any WebAssembly.Module will work fine for our tests; we just want to find out
// if it gives message or messageerror. So, we reuse one from the /wasm/ tests.
async function createWasmModule() {
const response = await fetch("/wasm/serialization/module/resources/incrementer.wasm");
const ab = await response.arrayBuffer();
return WebAssembly.compile(ab);
}