# 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 = """
<html>
  <head>
    <style>
      body {
        background-color: %(color)s;
      }
    </style>
  </head>
  <body>
    <center>
      <h1><a href="%(url)s">%(site)s</a></h1>
      <p><small>%(url)s</small>
    </center>
    <br />
    %(iframe_html)s
  </body>
</html>
"""

IFRAME_FRAGMENT = """
<iframe src="%(src)s" width="%(width)s" height="%(height)s">
</iframe>
"""

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