blob: 78f8126c9cf7f8ae9f2b0d34e17d76ad51784811 [file] [log] [blame]
<html>
<!-- This page can create whatever iframe 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 iframe:
<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 iframes:
<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 iframe 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://a.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 iframe, 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.
These options are supported:
allowfullscreen
allowpaymentrequest
allow-{featurename} - Adds {featurename} to the "allow" attribute
sandbox-{flagname} - Adds {flagname} to the "sandbox" attribute
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;
}
</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);
}
/**
* 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 iframe options and applies them to the provided element.
*/
function applyIFrameOptions(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;
}
/**
* 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 = canonicalizeSiteAndPort(frameTree.value, "");
// 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));
}
// 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 iframe.
var siteAndPort = canonicalizeSiteAndPort(frameTree.children[i].value, window.location.port);
var subtreeString = TreeParserUtil.flatten(frameTree.children[i]);
var 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 iframe.
var iframe = document.createElement('iframe');
iframe.src = url;
iframe.id = "child-" + i;
iframe.style.borderColor = borderColorForSite(siteAndPort);
iframe.width = frameTree.children[i].layoutX;
iframe.height = frameTree.children[i].layoutY;
// If needed, position the iframe.
if (renderOutsideParentView(frameTree.children[i].attributes)) {
iframe.style.position = "fixed";
iframe.style.top = "150%";
iframe.style.left = "150%";
}
// Apply any additional attributes.
applyIFrameOptions(iframe, frameTree.children[i].attributes);
// Add the iframe to the document.
document.body.appendChild(iframe);
}
}
main();
</script>
</body></html>