228 lines
7.7 KiB
HTML
228 lines
7.7 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
<title>Bounce!</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
</head>
|
|
|
|
<body>
|
|
<p id="test-config"></p>
|
|
<script>
|
|
const SET_STATE_HANDLERS = {
|
|
"cookie-client": setCookie,
|
|
"localStorage": setLocalStorage,
|
|
"indexedDB": setIndexedDB,
|
|
};
|
|
|
|
function setCookie(id, isThirdParty) {
|
|
let cookie = document.cookie;
|
|
if (cookie) {
|
|
console.info("Received cookie", cookie);
|
|
} else {
|
|
let newCookie = `id=${id};`;
|
|
if(isThirdParty) {
|
|
// If we're in the third-party context request a partitioned cookies
|
|
// for compatibility with CHIPS / 3rd party cookies being blocked by
|
|
// default.
|
|
newCookie += "SameSite=None; Secure; Partitioned;";
|
|
}
|
|
console.info("Setting new cookie", newCookie);
|
|
document.cookie = newCookie;
|
|
}
|
|
}
|
|
|
|
function setLocalStorage(id) {
|
|
let entry = localStorage.getItem("id");
|
|
if (entry) {
|
|
console.info("Found localStorage entry. id", entry);
|
|
} else {
|
|
console.info("Setting new localStorage entry. id", id);
|
|
localStorage.setItem(id, id);
|
|
}
|
|
}
|
|
|
|
function setIndexedDB() {
|
|
return new Promise((resolve, reject) => {
|
|
let request = window.indexedDB.open("bounce", 1);
|
|
request.onsuccess = () => {
|
|
console.info("Opened indexedDB");
|
|
resolve()
|
|
};
|
|
request.onerror = (event) => {
|
|
console.error("Error opening indexedDB", event);
|
|
reject();
|
|
};
|
|
request.onupgradeneeded = (event) => {
|
|
console.info("Initializing indexedDB");
|
|
let db = event.target.result;
|
|
db.createObjectStore("bounce");
|
|
};
|
|
});
|
|
}
|
|
|
|
function setIndexedDBInWorker(nested = false) {
|
|
let worker = new Worker("file_web_worker.js");
|
|
let msg = nested ? "setIndexedDBNested" : "setIndexedDB";
|
|
worker.postMessage(msg);
|
|
return new Promise((resolve, reject) => {
|
|
worker.onmessage = () => {
|
|
console.info("IndexedDB set in worker");
|
|
resolve();
|
|
};
|
|
worker.onerror = (event) => {
|
|
console.error("Error setting indexedDB in worker", event);
|
|
reject();
|
|
};
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Waits for the specified frame element to be ready.
|
|
*
|
|
* @param {HTMLIFrameElement} frameElement - The frame element to wait for.
|
|
* @param {boolean} waitForReadyMessage - Whether to wait for a "ready" message
|
|
* from the frame. If false, the promise will resolve when the frame is loaded.
|
|
* @returns {Promise<void>} - A promise that resolves when the frame is ready.
|
|
*/
|
|
function waitForFrameReady(frameElement, waitForReadyMessage = false) {
|
|
if (waitForReadyMessage) {
|
|
return new Promise((resolve) => {
|
|
window.addEventListener("message", (event) => {
|
|
if (event.data === "ready" && event.source === frameElement.contentWindow) {
|
|
resolve();
|
|
}
|
|
}, { once: true });
|
|
});
|
|
}
|
|
return new Promise((resolve) => {
|
|
frameElement.addEventListener("load", resolve, { once: true });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Waits for the specified image element to be loaded.
|
|
* @param {HTMLImageElement} img - The image element to wait for.
|
|
* @returns {Promise<void>} - A promise that resolves when the image is loaded.
|
|
*/
|
|
function waitForImageLoad(img) {
|
|
return new Promise((resolve) => {
|
|
img.addEventListener('load', () => {
|
|
resolve();
|
|
}, { once: true });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set a state in a child frame. The caller determines the host and thus
|
|
* whether the frame is same or cross-site with this page
|
|
*
|
|
* @param {string} frameURI - The URI of the frame to embed.
|
|
* @returns {Promise} - A promise that resolves when the iframe has loaded
|
|
* and set state.
|
|
*/
|
|
async function setStateInFrame(frameURI) {
|
|
// Embed self
|
|
let iframe = document.createElement("iframe");
|
|
|
|
let src = new URL(frameURI);
|
|
|
|
// Remove search params we don't need for the iframe.
|
|
src.searchParams.delete("target");
|
|
src.searchParams.delete("redirectDelay");
|
|
// Override potential 3xx codes to avoid the iframe redirecting if its
|
|
// pointed to file_bounce.sjs.
|
|
src.searchParams.set("statusCode", 200);
|
|
// Deleting this avoids infinite recursion. We don't want to spawn more
|
|
// nested frames.
|
|
src.searchParams.delete("setStateInFrameWithURI");
|
|
iframe.src = src.href;
|
|
|
|
// The frame only dispatches a ready message if this helper is used is
|
|
// loaded. Otherwise just wait for load.
|
|
let waitForReadyMessage = src.pathname.endsWith("file_bounce.html");
|
|
let frameReadyPromise = waitForFrameReady(iframe, waitForReadyMessage);
|
|
document.body.appendChild(iframe);
|
|
|
|
await frameReadyPromise;
|
|
}
|
|
|
|
/**
|
|
* Set a cookie by embedding an image which points to a server responding
|
|
* with a cookie header. The caller determines the host and thus whether the
|
|
* frame is same or cross-site with this page.
|
|
*
|
|
* @param {string} imgURI - The URI of the image to embed.
|
|
* @returns {Promise} - A promise that resolves when the image has loaded
|
|
* and the cookie has been set.
|
|
*/
|
|
async function setCookieViaImg(imgURI) {
|
|
// Set a cookie in a child frame via an image.
|
|
let img = new Image();
|
|
img.src = imgURI;
|
|
let loadPromise = waitForImageLoad(img);
|
|
document.body.appendChild(img);
|
|
await loadPromise
|
|
}
|
|
|
|
// Wrap the entire block so we can run async code.
|
|
(async () => {
|
|
let url = new URL(location.href);
|
|
// Display the test config in the body.
|
|
document.getElementById("test-config").innerText = JSON.stringify(Object.fromEntries(url.searchParams), null, 2);
|
|
|
|
let setStateInFrameWithURI = url.searchParams.get("setStateInFrameWithURI");
|
|
let setCookieViaImageWithURI = url.searchParams.get("setCookieViaImageWithURI");
|
|
|
|
if (setStateInFrameWithURI) {
|
|
// Set state in a child frame.
|
|
await setStateInFrame(setStateInFrameWithURI);
|
|
} else if(setCookieViaImageWithURI) {
|
|
await setCookieViaImg(setCookieViaImageWithURI);
|
|
} else if(url.searchParams.get("setStateInWebWorker") === "true") {
|
|
// Set state in a worker.
|
|
await setIndexedDBInWorker();
|
|
} else if(url.searchParams.get("setStateInNestedWebWorker") === "true") {
|
|
// Set state in a nested worker.
|
|
await setIndexedDBInWorker(true);
|
|
} else {
|
|
// Set a state in this window.
|
|
let setState = url.searchParams.get("setState");
|
|
if (setState) {
|
|
let id = Math.random().toString();
|
|
|
|
let handler = SET_STATE_HANDLERS[setState];
|
|
if (!handler) {
|
|
throw new Error("Unknown state handler: " + setState);
|
|
}
|
|
let isThirdParty = url.searchParams.get("isThirdParty") === "true";
|
|
await handler(id, isThirdParty);
|
|
}
|
|
}
|
|
|
|
window.parent.postMessage('ready', '*');
|
|
|
|
// Redirect to the target URL after a delay.
|
|
// If no target is specified, do nothing.
|
|
let redirectDelay = url.searchParams.get("redirectDelay");
|
|
if (redirectDelay != null) {
|
|
redirectDelay = Number.parseInt(redirectDelay);
|
|
} else {
|
|
redirectDelay = 50;
|
|
}
|
|
|
|
let target = url.searchParams.get("target");
|
|
if (target) {
|
|
console.info("Redirecting to", target);
|
|
setTimeout(() => {
|
|
location.href = target;
|
|
}, redirectDelay);
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|