1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237
  238
  239
  240
  241
  242
  243
  244
  245
  246
  247
  248
  249
  250
  251
  252
  253
  254
  255
  256
  257
  258
  259
  260
  261
  262
  263
  264
  265
  266
  267
  268
  269
  270
  271
  272
  273
  274
  275
  276
  277
  278
  279
  280
  281
  282
  283
  284
  285
  286
  287
  288
  289
  290
  291
  292
  293
  294
  295
  296
  297
  298
  299
  300
  301
  302
  303
  304
  305
  306
  307
  308
  309
  310
  311
  312
  313
  314
  315
  316
  317
  318
  319
  320
  321
  322
  323
  324
  325
  326
  327
  328
  329
  330
  331
  332
  333
  334
  335
  336
  337
  338
  339
  340
  341
  342
  343
  344
  345
  346
  347
  348
  349
  350
  351
  352
  353
  354
  355
  356
  357
  358
  359
  360
  361
  362
  363
  364
  365
  366
  367
  368
  369
  370
  371
  372
  373
  374
  375
  376
  377
  378
  379
  380
  381

content / test / data / cross_site_iframe_factory.html [blame]

<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>