iframe_server.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Test server for generating nested iframes with different sites.
6
7Very simple python server for creating a bunch of iframes. The page generation
8is randomized based on query parameters.  See the __init__ function of the
9Params class for a description of the parameters.
10
11This server relies on gevent. On Ubuntu, install it via:
12
13  sudo apt-get install python-gevent
14
15Run the server using
16
17  python iframe_server.py
18
19To use the server, run chrome as follows:
20
21  google-chrome --host-resolver-rules='map *.invalid 127.0.0.1'
22
23Change 127.0.0.1 to be the IP of the machine this server is running on. Then
24in this chrome instance, navigate to any domain in .invalid
25(eg., http://1.invalid:8090) to run this test.
26
27"""
28
29import colorsys
30import copy
31import random
32import urllib
33import urlparse
34
35from gevent import pywsgi # pylint: disable=F0401
36
37MAIN_PAGE = """
38<html>
39  <head>
40    <style>
41      body {
42        background-color: %(color)s;
43      }
44    </style>
45  </head>
46  <body>
47    <center>
48      <h1><a href="%(url)s">%(site)s</a></h1>
49      <p><small>%(url)s</small>
50    </center>
51    <br />
52    %(iframe_html)s
53  </body>
54</html>
55"""
56
57IFRAME_FRAGMENT = """
58<iframe src="%(src)s" width="%(width)s" height="%(height)s">
59</iframe>
60"""
61
62class Params(object):
63  """Simple object for holding parameters"""
64  def __init__(self, query_dict):
65    # Basic params:
66    #  nframes is how many frames per page.
67    #  nsites is how many sites to random choose out of.
68    #  depth is how deep to make the frame tree
69    #  pattern specifies how the sites are layed out per depth. An empty string
70    #      uses a random N = [0, nsites] each time to generate a N.invalid URL.
71    #      Otherwise sepcify with single letters like 'ABCA' and frame
72    #      A.invalid will embed B.invalid will embed C.invalid will embed A.
73    #  jitter is the amount of randomness applied to nframes and nsites.
74    #      Should be from [0,1]. 0.0 means no jitter.
75    #  size_jitter is like jitter, but for width and height.
76    self.nframes = int(query_dict.get('nframes', [4] )[0])
77    self.nsites = int(query_dict.get('nsites', [10] )[0])
78    self.depth = int(query_dict.get('depth', [1] )[0])
79    self.jitter = float(query_dict.get('jitter', [0] )[0])
80    self.size_jitter = float(query_dict.get('size_jitter', [0.5] )[0])
81    self.pattern = query_dict.get('pattern', [''] )[0]
82    self.pattern_pos = int(query_dict.get('pattern_pos', [0] )[0])
83
84    # Size parameters. Values are percentages.
85    self.width = int(query_dict.get('width', [60])[0])
86    self.height = int(query_dict.get('height', [50])[0])
87
88    # Pass the random seed so our pages are reproduceable.
89    self.seed = int(query_dict.get('seed',
90                                   [random.randint(0, 2147483647)])[0])
91
92
93def get_site(urlpath):
94  """Takes a urlparse object and finds its approximate site.
95
96  Site is defined as registered domain name + scheme. We approximate
97  registered domain name by preserving the last 2 elements of the DNS
98  name. This breaks for domains like co.uk.
99  """
100  no_port = urlpath.netloc.split(':')[0]
101  host_parts = no_port.split('.')
102  site_host = '.'.join(host_parts[-2:])
103  return '%s://%s' % (urlpath.scheme, site_host)
104
105
106def generate_host(rand, params):
107  """Generates the host to be used as an iframes source.
108
109  Uses the .invalid domain to ensure DNS will not resolve to any real
110  address.
111  """
112  if params.pattern:
113    host = params.pattern[params.pattern_pos]
114    params.pattern_pos = (params.pattern_pos + 1) % len(params.pattern)
115  else:
116    host = rand.randint(1, apply_jitter(rand, params.jitter, params.nsites))
117  return '%s.invalid' % host
118
119
120def apply_jitter(rand, jitter, n):
121  """Reduce n by random amount from [0, jitter]. Ensures result is >=1."""
122  if jitter <= 0.001:
123    return n
124  v = n - int(n * rand.uniform(0, jitter))
125  if v:
126    return v
127  else:
128    return 1
129
130
131def get_color_for_site(site):
132  """Generate a stable (and pretty-ish) color for a site."""
133  val = hash(site)
134  # The constants below are arbitrary chosen emperically to look "pretty."
135  # HSV is used because it is easier to control the color than RGB.
136  # Reducing the H to 0.6 produces a good range of colors. Preserving
137  # > 0.5 saturation and value means the colors won't be too washed out.
138  h = (val % 100)/100.0 * 0.6
139  s = 1.0 - (int(val/100) % 100)/200.
140  v = 1.0 - (int(val/10000) % 100)/200.0
141  (r, g, b) = colorsys.hsv_to_rgb(h, s, v)
142  return 'rgb(%d, %d, %d)' % (int(r * 255), int(g * 255), int(b * 255))
143
144
145def make_src(scheme, netloc, path, params):
146  """Constructs the src url that will recreate the given params."""
147  if path == '/':
148    path = ''
149  return '%(scheme)s://%(netloc)s%(path)s?%(params)s' % {
150      'scheme': scheme,
151      'netloc': netloc,
152      'path': path,
153      'params': urllib.urlencode(params.__dict__),
154      }
155
156
157def make_iframe_html(urlpath, params):
158  """Produces the HTML fragment for the iframe."""
159  if (params.depth <= 0):
160    return ''
161  # Ensure a stable random number per iframe.
162  rand = random.Random()
163  rand.seed(params.seed)
164
165  netloc_paths = urlpath.netloc.split(':')
166  netloc_paths[0] = generate_host(rand, params)
167
168  width = apply_jitter(rand, params.size_jitter, params.width)
169  height = apply_jitter(rand, params.size_jitter, params.height)
170  iframe_params = {
171      'src': make_src(urlpath.scheme, ':'.join(netloc_paths),
172                      urlpath.path, params),
173      'width': '%d%%' % width,
174      'height': '%d%%' % height,
175      }
176  return IFRAME_FRAGMENT % iframe_params
177
178
179def create_html(environ):
180  """Creates the current HTML page. Also parses out query parameters."""
181  urlpath = urlparse.urlparse('%s://%s%s?%s' % (
182      environ['wsgi.url_scheme'],
183      environ['HTTP_HOST'],
184      environ['PATH_INFO'],
185      environ['QUERY_STRING']))
186  site = get_site(urlpath)
187  params = Params(urlparse.parse_qs(urlpath.query))
188
189  rand = random.Random()
190  rand.seed(params.seed)
191
192  iframe_htmls = []
193  for frame in xrange(0, apply_jitter(rand, params.jitter, params.nframes)):
194    # Copy current parameters into iframe and make modifications
195    # for the recursive generation.
196    iframe_params = copy.copy(params)
197    iframe_params.depth = params.depth - 1
198    # Base the new seed off the current seed, but have it skip enough that
199    # different frame trees are unlikely to collide. Numbers and skips
200    # not chosen in any scientific manner at all.
201    iframe_params.seed = params.seed + (frame + 1) * (
202        1000000 + params.depth + 333)
203    iframe_htmls.append(make_iframe_html(urlpath, iframe_params))
204  template_params = dict(params.__dict__)
205  template_params.update({
206      'color': get_color_for_site(site),
207      'iframe_html': '\n'.join(iframe_htmls),
208      'site': site,
209      'url': make_src(urlpath.scheme, urlpath.netloc, urlpath.path, params),
210      })
211  return MAIN_PAGE % template_params
212
213
214def application(environ, start_response):
215  start_response('200 OK', [('Content-Type', 'text/html')])
216  if environ['PATH_INFO'] == '/favicon.ico':
217    yield ''
218  else:
219    yield create_html(environ)
220
221
222server = pywsgi.WSGIServer(('', 8090), application)
223
224server.serve_forever()
225