<html>
<!-- This page can create whatever frame structure you want, across whatever
sites you want. This is useful for testing site isolation.
Example usage in a browsertest, explained:
GURL url = embedded_test_server()->
GetURL("a.com", "/cross_site_iframe_factory.html?a(b(c,d))");
When you navigate to the above URL, the outer document (on a.com) will create a
single frame:
<iframe id="child-0" src="http://b.com:1234/cross_site_iframe_factory.html?b(c(),d())">
Inside of which, then, are created the two leaf frames:
<iframe id="child-0" src="http://c.com:1234/cross_site_iframe_factory.html?c()">
<iframe id="child-1" src="http://d.com:1234/cross_site_iframe_factory.html?d()">
Add frame options by enclosing them in '{' and '}' characters after the
hostname (multiple options can be separated with commas):
cross_site_iframe_factory.html?a(b{allowfullscreen}(),c{sandbox-allow-scripts}(d))
Will create two iframes:
<iframe id="child-0" src="http://b.com:1234/cross_site_iframe_factory.html?b()" allowfullscreen>
<iframe id="child-1" src="http://c.com:1234/cross_site_iframe_factory.html?c{sandbox-allow-scripts}(d())" sandbox="allow-scripts">
To specify the site for each frame, you can use a simple identifier like "a"
or "b", and ".com" will be automatically appended. You can also specify a port
number to use for each site by using a label like "a:12345". If no port number
is given, the port will default to the port number of the top level frame.
You can also specify an arbitrary full URL for any bottommost leaf frame.
These options are supported:
allowfullscreen
allowpaymentrequest
allow-{featurename} - Adds {featurename} to the "allow" attribute
sandbox-{flagname} - Adds {flagname} to the "sandbox" attribute
with-shared-worker - Creates a shared worker in each frame.
with-worker - Creates a dedicated worker in each frame.
fenced - Creates a <fencedframe> instead of an <iframe>.
To make this page work, your browsertest needs a MockHostResolver, like:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
}
You can play around with the arguments by loading this page via file://, but
you probably won't get the same process behavior as if you loaded via http. -->
<head>
<title>Cross-site iframe factory</title>
<style>
body {
font-family: "DejaVu Sans", Sans-Serif;
text-align: center;
}
iframe {
border-radius: 7px;
border-style: solid;
vertical-align: top;
margin: 2px;
box-shadow: 2px 2px 2px #888888;
}
fencedframe {
border-radius: 7px;
border-style: solid;
vertical-align: top;
margin: 2px;
box-shadow: 2px 2px 2px #888888;
}
</style>
</head>
<body>
<h2 id='siteNameHeading'></h2>
<script src='tree_parser_util.js'></script>
<script type='text/javascript'>
/**
* Determines a random pastel-ish color from the first character of a string.
*/
function pastelColorForFirstCharacter(seedString, lightness) {
// Map the first character to an index. This could be negative, we don't
// really care.
var index = seedString.charCodeAt(0) - 'a'.charCodeAt(0);
// If the first character is 'a', this will the the starting color.
var hueOfA = 200; // Spoiler alert: it's blue.
// Color palette generation articles suggest that spinning the hue wheel by
// the golden ratio yields a magically nice color distribution. Something
// about sunflower seeds. I am skeptical of the rigor of that claim (probably
// any irrational number at a slight offset from 2/3 would do) but it does
// look pretty.
var phi = 2 / (1 + Math.pow(5, .5));
var hue = Math.round((360 * index * phi + hueOfA) % 360);
return 'hsl(' + hue + ', 60%, ' + Math.round(100 * lightness) + '%)';
}
function backgroundColorForSite(site) {
// Light pastel.
return pastelColorForFirstCharacter(site, .75);
}
function borderColorForSite(site) {
// Darker color in the same hue has the background.
return pastelColorForFirstCharacter(site, .32);
}
function isUrl(siteString) {
try {
var url = new URL(siteString);
} catch (e) {
if (e instanceof TypeError)
return false;
}
return true;
}
/**
* Extract the specified port number, if any. Returns empty string if not
* specified.
*/
function sitePortNumber(siteString) {
let index = siteString.indexOf(':');
if (index == -1)
return ""
return siteString.substring(index + 1);
}
/**
* Adds ".com" to an argument if it doesn't already have a top level domain.
* This cuts down on noise in the query string, letting you use single-letter
* names. Adds the specified port number, if any, or the default port otherwise.
*/
function canonicalizeSiteAndPort(siteString, defaultPort) {
var portNumber = sitePortNumber(siteString) || defaultPort;
var hostName = siteString.split(':')[0];
if (hostName !== "localhost" && hostName.indexOf('.') == -1)
hostName = hostName + '.com';
return hostName + (portNumber ? ':' + portNumber : "");
}
/**
* Parses the list of frame options and applies them to the provided element.
*/
function applyFrameOptions(element, options) {
var sandbox = false;
var sandboxArguments = [];
var allowFeatures = [];
for (var option of options) {
if (option == "allowfullscreen") {
element.allowFullscreen = true;
}
if (option == "allowpaymentrequest") {
element.allowPaymentRequest = true;
}
if (option == "sandbox") {
sandbox = true;
}
if (option.startsWith("sandbox-")) {
sandbox = true;
sandboxArguments.push(option.slice(8));
}
if (option.startsWith("allow-")) {
allowFeatures.push(option.slice(6))
}
}
if (sandbox) {
element.sandbox = sandboxArguments.join(" ");
}
if (allowFeatures.length) {
element.allow = allowFeatures.join(" ");
}
}
/**
* Determines whether or not text should be rendered by checking whether
* "no-text-render" is among the list of attributes.
*/
function shouldRenderText(attributes) {
for (var attribute of attributes) {
if (attribute == "no-text-render") {
return false;
}
}
return true;
}
/**
* Determines whether or not the frame should be positioned outside of
* its parent frame by checking whether "out-of-view" is among the list
* of attributes.
*/
function renderOutsideParentView(attributes) {
for (var attribute of attributes) {
if (attribute == "out-of-view") {
return true;
}
}
return false;
}
/**
* Determines whether or not the frame should create a worker by checking
* whether "with-worker" is among the list of attributes.
*/
function shouldCreateWorker(attributes) {
for (var attribute of attributes) {
if (attribute == "with-worker") {
return true;
}
}
return false;
}
/**
* Determines whether or not the frame should create a shared worker by checking
* whether "with-shared-worker" is among the list of attributes.
*/
function shouldCreateSharedWorker(attributes) {
for (var attribute of attributes) {
if (attribute == "with-shared-worker") {
return true;
}
}
return false;
}
/**
* Determines whether or not the frame created should be a fenced frame by
* checking whether "fenced" is among the list of attributes.
*/
function shouldCreateFencedFrame(attributes) {
for (var attribute of attributes) {
if (attribute == "fenced") {
return true;
}
}
return false;
}
/**
* Simple recursive layout heuristic, since frames can't size themselves.
* This scribbles .layoutX and .layoutY properties into |tree|.
*/
function layout(tree) {
// Step 1: layout children.
var numFrames = tree.children.length;
for (var i = 0; i < numFrames; i++) {
layout(tree.children[i]);
}
// Step 2: find largest child.
var largestChildX = 0;
var largestChildY = 0;
for (var i = 0; i < numFrames; i++) {
largestChildX = Math.max(largestChildX, tree.children[i].layoutX);
largestChildY = Math.max(largestChildY, tree.children[i].layoutY);
}
// Step 3: Tweakable control parameters.
var minX = 110; // Should be wide enough to fit a decent sized domain.
var minY = 110; // Could be less, but squares look nice.
var extraYPerLevel = 50; // Needs to be tall enough to fit a line of text.
var extraXPerLevel = 50; // Could be less, but squares look nice.
// Account for padding around each <iframe>.
largestChildX += 30;
largestChildY += 30;
// Step 4: Assume a gridSizeX-by-gridSizeY layout that's big enough to fit if
// all children were the size of the largest one.
var gridSizeX = Math.ceil(Math.sqrt(numFrames));
var gridSizeY = Math.round(Math.sqrt(numFrames));
tree.layoutX = Math.max(gridSizeX * largestChildX + extraXPerLevel, minX);
tree.layoutY = Math.max(gridSizeY * largestChildY + extraYPerLevel, minY);
}
function main() {
var goCrossSite = !window.location.protocol.startsWith('file');
var queryString = decodeURIComponent(window.location.search.substring(1));
var frameTree = TreeParserUtil.parse(queryString);
var currentSite = isUrl(frameTree.value)
? frameTree.value
: canonicalizeSiteAndPort(frameTree.value, "");
// If the top-level site doesn't match the hostname, the user is
// probably making a mistake, e.g. hostname is "a.test" and it's
// likely that subframes are incorrectly being left to default to
// ".com". In that case we bail out and refuse to create subframes,
// which should hopefully cause the test to fail.
if (currentSite != window.location.hostname) {
console.error(`The first hostname in the query-string does not match the` +
` hostname of the URL: ${currentSite} != ${window.location.hostname}`);
return;
}
// Render text unless the attribute "no-text-render" is specified, in which
// case load some bytes anyway in order to trigger histogram recording for
// ad metrics tests.
if (shouldRenderText(frameTree.attributes)) {
document.getElementById('siteNameHeading').appendChild(
document.createTextNode(currentSite));
}
// Create a worker with matching query-string if the "with-worker" attribute
// is specified.
if (shouldCreateWorker(frameTree.attributes)) {
let worker_url = 'workers/empty.js';
worker_url += '?' + queryString;
console.log(worker_url);
self.worker = new Worker(worker_url);
}
// Create a shared worker with matching query-string if the "with-worker"
// attribute is specified.
if (shouldCreateSharedWorker(frameTree.attributes)) {
let worker_url = 'workers/empty.js';
worker_url += '?' + queryString;
console.log(worker_url);
self.worker = new SharedWorker(worker_url);
}
// Apply style to the current document.
document.body.style.backgroundColor = backgroundColorForSite(currentSite);
// Determine how big the children should be (using a very rough heuristic).
layout(frameTree);
for (var i = 0; i < frameTree.children.length; i++) {
// Compute the URL for this frame.
let url = frameTree.children[i].value;
let siteAndPort = url;
if (!isUrl(url)) {
siteAndPort = canonicalizeSiteAndPort(url, window.location.port);
const subtreeString = TreeParserUtil.flatten(frameTree.children[i]);
url = '';
url += window.location.protocol + '//'; // scheme (preserved)
url += goCrossSite ? siteAndPort : window.location.host; // host and port
url += window.location.pathname; // path (preserved)
url += '?' + encodeURIComponent(subtreeString); // query
}
// Construct the frame.
const frame_type = shouldCreateFencedFrame(frameTree.children[i].attributes) ?
'fencedframe' : 'iframe';
var frame = document.createElement(frame_type);
if (frame_type == 'iframe') {
frame.src = url;
} else {
frame.config = new FencedFrameConfig(url);
}
frame.id = "child-" + i;
frame.style.borderColor = borderColorForSite(siteAndPort);
frame.width = frameTree.children[i].layoutX;
frame.height = frameTree.children[i].layoutY;
// If needed, position the frame.
if (renderOutsideParentView(frameTree.children[i].attributes)) {
frame.style.position = "fixed";
frame.style.top = "150%";
frame.style.left = "150%";
}
// Apply any additional attributes.
applyFrameOptions(frame, frameTree.children[i].attributes);
// Add the frame to the document.
document.body.appendChild(frame);
}
}
main();
</script>
</body></html>