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() 225