# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Test server for generating nested iframes with different sites.
Very simple python server for creating a bunch of iframes. The page generation
is randomized based on query parameters. See the __init__ function of the
Params class for a description of the parameters.
This server relies on gevent. On Ubuntu, install it via:
sudo apt-get install python-gevent
Run the server using
python iframe_server.py
To use the server, run chrome as follows:
google-chrome --host-resolver-rules='map *.invalid 127.0.0.1'
Change 127.0.0.1 to be the IP of the machine this server is running on. Then
in this chrome instance, navigate to any domain in .invalid
(eg., http://1.invalid:8090) to run this test.
"""
import colorsys
import copy
import random
import urllib
import urlparse
from gevent import pywsgi # pylint: disable=F0401
MAIN_PAGE = """
%(url)s
%(iframe_html)s
"""
IFRAME_FRAGMENT = """
"""
class Params(object):
"""Simple object for holding parameters"""
def __init__(self, query_dict):
# Basic params:
# nframes is how many frames per page.
# nsites is how many sites to random choose out of.
# depth is how deep to make the frame tree
# pattern specifies how the sites are layed out per depth. An empty string
# uses a random N = [0, nsites] each time to generate a N.invalid URL.
# Otherwise sepcify with single letters like 'ABCA' and frame
# A.invalid will embed B.invalid will embed C.invalid will embed A.
# jitter is the amount of randomness applied to nframes and nsites.
# Should be from [0,1]. 0.0 means no jitter.
# size_jitter is like jitter, but for width and height.
self.nframes = int(query_dict.get('nframes', [4] )[0])
self.nsites = int(query_dict.get('nsites', [10] )[0])
self.depth = int(query_dict.get('depth', [1] )[0])
self.jitter = float(query_dict.get('jitter', [0] )[0])
self.size_jitter = float(query_dict.get('size_jitter', [0.5] )[0])
self.pattern = query_dict.get('pattern', [''] )[0]
self.pattern_pos = int(query_dict.get('pattern_pos', [0] )[0])
# Size parameters. Values are percentages.
self.width = int(query_dict.get('width', [60])[0])
self.height = int(query_dict.get('height', [50])[0])
# Pass the random seed so our pages are reproduceable.
self.seed = int(query_dict.get('seed',
[random.randint(0, 2147483647)])[0])
def get_site(urlpath):
"""Takes a urlparse object and finds its approximate site.
Site is defined as registered domain name + scheme. We approximate
registered domain name by preserving the last 2 elements of the DNS
name. This breaks for domains like co.uk.
"""
no_port = urlpath.netloc.split(':')[0]
host_parts = no_port.split('.')
site_host = '.'.join(host_parts[-2:])
return '%s://%s' % (urlpath.scheme, site_host)
def generate_host(rand, params):
"""Generates the host to be used as an iframes source.
Uses the .invalid domain to ensure DNS will not resolve to any real
address.
"""
if params.pattern:
host = params.pattern[params.pattern_pos]
params.pattern_pos = (params.pattern_pos + 1) % len(params.pattern)
else:
host = rand.randint(1, apply_jitter(rand, params.jitter, params.nsites))
return '%s.invalid' % host
def apply_jitter(rand, jitter, n):
"""Reduce n by random amount from [0, jitter]. Ensures result is >=1."""
if jitter <= 0.001:
return n
v = n - int(n * rand.uniform(0, jitter))
if v:
return v
else:
return 1
def get_color_for_site(site):
"""Generate a stable (and pretty-ish) color for a site."""
val = hash(site)
# The constants below are arbitrary chosen emperically to look "pretty."
# HSV is used because it is easier to control the color than RGB.
# Reducing the H to 0.6 produces a good range of colors. Preserving
# > 0.5 saturation and value means the colors won't be too washed out.
h = (val % 100)/100.0 * 0.6
s = 1.0 - (int(val/100) % 100)/200.
v = 1.0 - (int(val/10000) % 100)/200.0
(r, g, b) = colorsys.hsv_to_rgb(h, s, v)
return 'rgb(%d, %d, %d)' % (int(r * 255), int(g * 255), int(b * 255))
def make_src(scheme, netloc, path, params):
"""Constructs the src url that will recreate the given params."""
if path == '/':
path = ''
return '%(scheme)s://%(netloc)s%(path)s?%(params)s' % {
'scheme': scheme,
'netloc': netloc,
'path': path,
'params': urllib.urlencode(params.__dict__),
}
def make_iframe_html(urlpath, params):
"""Produces the HTML fragment for the iframe."""
if (params.depth <= 0):
return ''
# Ensure a stable random number per iframe.
rand = random.Random()
rand.seed(params.seed)
netloc_paths = urlpath.netloc.split(':')
netloc_paths[0] = generate_host(rand, params)
width = apply_jitter(rand, params.size_jitter, params.width)
height = apply_jitter(rand, params.size_jitter, params.height)
iframe_params = {
'src': make_src(urlpath.scheme, ':'.join(netloc_paths),
urlpath.path, params),
'width': '%d%%' % width,
'height': '%d%%' % height,
}
return IFRAME_FRAGMENT % iframe_params
def create_html(environ):
"""Creates the current HTML page. Also parses out query parameters."""
urlpath = urlparse.urlparse('%s://%s%s?%s' % (
environ['wsgi.url_scheme'],
environ['HTTP_HOST'],
environ['PATH_INFO'],
environ['QUERY_STRING']))
site = get_site(urlpath)
params = Params(urlparse.parse_qs(urlpath.query))
rand = random.Random()
rand.seed(params.seed)
iframe_htmls = []
for frame in xrange(0, apply_jitter(rand, params.jitter, params.nframes)):
# Copy current parameters into iframe and make modifications
# for the recursive generation.
iframe_params = copy.copy(params)
iframe_params.depth = params.depth - 1
# Base the new seed off the current seed, but have it skip enough that
# different frame trees are unlikely to collide. Numbers and skips
# not chosen in any scientific manner at all.
iframe_params.seed = params.seed + (frame + 1) * (
1000000 + params.depth + 333)
iframe_htmls.append(make_iframe_html(urlpath, iframe_params))
template_params = dict(params.__dict__)
template_params.update({
'color': get_color_for_site(site),
'iframe_html': '\n'.join(iframe_htmls),
'site': site,
'url': make_src(urlpath.scheme, urlpath.netloc, urlpath.path, params),
})
return MAIN_PAGE % template_params
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
if environ['PATH_INFO'] == '/favicon.ico':
yield ''
else:
yield create_html(environ)
server = pywsgi.WSGIServer(('', 8090), application)
server.serve_forever()