Source code
Revision control
Copy as Markdown
Other Tools
<!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>