[email protected] | c6e16cd | 2013-03-11 23:16:17 | [diff] [blame] | 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 | |
| 7 | Very simple python server for creating a bunch of iframes. The page generation |
| 8 | is randomized based on query parameters. See the __init__ function of the |
| 9 | Params class for a description of the parameters. |
| 10 | |
| 11 | This server relies on gevent. On Ubuntu, install it via: |
| 12 | |
| 13 | sudo apt-get install python-gevent |
| 14 | |
| 15 | Run the server using |
| 16 | |
| 17 | python iframe_server.py |
| 18 | |
| 19 | To use the server, run chrome as follows: |
| 20 | |
| 21 | google-chrome --host-resolver-rules='map *.invalid 127.0.0.1' |
| 22 | |
| 23 | Change 127.0.0.1 to be the IP of the machine this server is running on. Then |
| 24 | in this chrome instance, navigate to any domain in .invalid |
| 25 | (eg., http://1.invalid:8090) to run this test. |
| 26 | |
| 27 | """ |
| 28 | |
| 29 | import colorsys |
| 30 | import copy |
| 31 | import random |
| 32 | import urllib |
| 33 | import urlparse |
| 34 | |
| 35 | from gevent import pywsgi # pylint: disable=F0401 |
| 36 | |
| 37 | MAIN_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 | |
| 57 | IFRAME_FRAGMENT = """ |
| 58 | <iframe src="%(src)s" width="%(width)s" height="%(height)s"> |
| 59 | </iframe> |
| 60 | """ |
| 61 | |
| 62 | class 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 | |
| 93 | def 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 | |
| 106 | def 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 | |
| 120 | def 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 | |
| 131 | def 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 | |
| 145 | def 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 | |
| 157 | def 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 | |
| 179 | def 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 | |
| 214 | def 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 | |
| 222 | server = pywsgi.WSGIServer(('', 8090), application) |
| 223 | |
| 224 | server.serve_forever() |