Home | History | Annotate | Download | only in browsertester
      1 # Copyright (c) 2011 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 import BaseHTTPServer
      6 import cgi
      7 import mimetypes
      8 import os
      9 import os.path
     10 import posixpath
     11 import SimpleHTTPServer
     12 import SocketServer
     13 import threading
     14 import time
     15 import urllib
     16 import urlparse
     17 
     18 class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
     19 
     20   def NormalizePath(self, path):
     21     path = path.split('?', 1)[0]
     22     path = path.split('#', 1)[0]
     23     path = posixpath.normpath(urllib.unquote(path))
     24     words = path.split('/')
     25 
     26     bad = set((os.curdir, os.pardir, ''))
     27     words = [word for word in words if word not in bad]
     28     # The path of the request should always use POSIX-style path separators, so
     29     # that the filename input of --map_file can be a POSIX-style path and still
     30     # match correctly in translate_path().
     31     return '/'.join(words)
     32 
     33   def translate_path(self, path):
     34     path = self.NormalizePath(path)
     35     if path in self.server.file_mapping:
     36       return self.server.file_mapping[path]
     37     for extra_dir in self.server.serving_dirs:
     38       # TODO(halyavin): set allowed paths in another parameter?
     39       full_path = os.path.join(extra_dir, os.path.basename(path))
     40       if os.path.isfile(full_path):
     41         return full_path
     42 
     43       # Try the complete relative path, not just a basename. This allows the
     44       # user to serve everything recursively under extra_dir, not just one
     45       # level deep.
     46       #
     47       # One use case for this is the Native Client SDK examples. The examples
     48       # expect to be able to access files as relative paths from the root of
     49       # the example directory.
     50       # Sometimes two subdirectories contain files with the same name, so
     51       # including all subdirectories in self.server.serving_dirs will not do
     52       # the correct thing; (i.e. the wrong file will be chosen, even though the
     53       # correct path was given).
     54       full_path = os.path.join(extra_dir, path)
     55       if os.path.isfile(full_path):
     56         return full_path
     57     if not path.endswith('favicon.ico') and not self.server.allow_404:
     58       self.server.listener.ServerError('Cannot find file \'%s\'' % path)
     59     return path
     60 
     61   def guess_type(self, path):
     62       # We store the extension -> MIME type mapping in the server instead of the
     63       # request handler so we that can add additional mapping entries via the
     64       # command line.
     65       base, ext = posixpath.splitext(path)
     66       if ext in self.server.extensions_mapping:
     67           return self.server.extensions_mapping[ext]
     68       ext = ext.lower()
     69       if ext in self.server.extensions_mapping:
     70           return self.server.extensions_mapping[ext]
     71       else:
     72           return self.server.extensions_mapping['']
     73 
     74   def SendRPCResponse(self, response):
     75     self.send_response(200)
     76     self.send_header("Content-type", "text/plain")
     77     self.send_header("Content-length", str(len(response)))
     78     self.end_headers()
     79     self.wfile.write(response)
     80 
     81     # shut down the connection
     82     self.wfile.flush()
     83     self.connection.shutdown(1)
     84 
     85   def HandleRPC(self, name, query):
     86     kargs = {}
     87     for k, v in query.iteritems():
     88       assert len(v) == 1, k
     89       kargs[k] = v[0]
     90 
     91     l = self.server.listener
     92     try:
     93       response = getattr(l, name)(**kargs)
     94     except Exception, e:
     95       self.SendRPCResponse('%r' % (e,))
     96       raise
     97     else:
     98       self.SendRPCResponse(response)
     99 
    100   # For Last-Modified-based caching, the timestamp needs to be old enough
    101   # for the browser cache to be used (at least 60 seconds).
    102   # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
    103   # Often we clobber and regenerate files for testing, so this is needed
    104   # to actually use the browser cache.
    105   def send_header(self, keyword, value):
    106     if keyword == 'Last-Modified':
    107       last_mod_format = '%a, %d %b %Y %H:%M:%S GMT'
    108       old_value_as_t = time.strptime(value, last_mod_format)
    109       old_value_in_secs = time.mktime(old_value_as_t)
    110       new_value_in_secs = old_value_in_secs - 360
    111       value = time.strftime(last_mod_format,
    112                             time.localtime(new_value_in_secs))
    113     SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(self,
    114                                                           keyword,
    115                                                           value)
    116 
    117   def do_POST(self):
    118     # Backwards compatible - treat result as tuple without named fields.
    119     _, _, path, _, query, _ = urlparse.urlparse(self.path)
    120 
    121     self.server.listener.Log('POST %s (%s)' % (self.path, path))
    122     if path == '/echo':
    123       self.send_response(200)
    124       self.end_headers()
    125       data = self.rfile.read(int(self.headers.getheader('content-length')))
    126       self.wfile.write(data)
    127     else:
    128       self.send_error(404, 'File not found')
    129 
    130     self.server.ResetTimeout()
    131 
    132   def do_GET(self):
    133     # Backwards compatible - treat result as tuple without named fields.
    134     _, _, path, _, query, _ = urlparse.urlparse(self.path)
    135 
    136     tester = '/TESTER/'
    137     if path.startswith(tester):
    138       # If the path starts with '/TESTER/', the GET is an RPC call.
    139       name = path[len(tester):]
    140       # Supporting Python 2.5 prevents us from using urlparse.parse_qs
    141       query = cgi.parse_qs(query, True)
    142 
    143       self.server.rpc_lock.acquire()
    144       try:
    145         self.HandleRPC(name, query)
    146       finally:
    147         self.server.rpc_lock.release()
    148 
    149       # Don't reset the timeout.  This is not "part of the test", rather it's
    150       # used to tell us if the renderer process is still alive.
    151       if name == 'JavaScriptIsAlive':
    152         self.server.JavaScriptIsAlive()
    153         return
    154 
    155     elif path in self.server.redirect_mapping:
    156       dest = self.server.redirect_mapping[path]
    157       self.send_response(301, 'Moved')
    158       self.send_header('Location', dest)
    159       self.end_headers()
    160       self.wfile.write(self.error_message_format %
    161                        {'code': 301,
    162                         'message': 'Moved',
    163                         'explain': 'Object moved permanently'})
    164       self.server.listener.Log('REDIRECT %s (%s -> %s)' %
    165                                 (self.path, path, dest))
    166     else:
    167       self.server.listener.Log('GET %s (%s)' % (self.path, path))
    168       # A normal GET request for transferring files, etc.
    169       f = self.send_head()
    170       if f:
    171         self.copyfile(f, self.wfile)
    172         f.close()
    173 
    174     self.server.ResetTimeout()
    175 
    176   def copyfile(self, source, outputfile):
    177     # Bandwidth values <= 0.0 are considered infinite
    178     if self.server.bandwidth <= 0.0:
    179       return SimpleHTTPServer.SimpleHTTPRequestHandler.copyfile(
    180           self, source, outputfile)
    181 
    182     self.server.listener.Log('Simulating %f mbps server BW' %
    183                              self.server.bandwidth)
    184     chunk_size = 1500 # What size to use?
    185     bits_per_sec = self.server.bandwidth * 1000000
    186     start_time = time.time()
    187     data_sent = 0
    188     while True:
    189       chunk = source.read(chunk_size)
    190       if len(chunk) == 0:
    191         break
    192       cur_elapsed = time.time() - start_time
    193       target_elapsed = (data_sent + len(chunk)) * 8 / bits_per_sec
    194       if (cur_elapsed < target_elapsed):
    195         time.sleep(target_elapsed - cur_elapsed)
    196       outputfile.write(chunk)
    197       data_sent += len(chunk)
    198     self.server.listener.Log('Streamed %d bytes in %f s' %
    199                              (data_sent, time.time() - start_time))
    200 
    201   # Disable the built-in logging
    202   def log_message(self, format, *args):
    203     pass
    204 
    205 
    206 # The ThreadingMixIn allows the server to handle multiple requests
    207 # concurently (or at least as concurently as Python allows).  This is desirable
    208 # because server sockets only allow a limited "backlog" of pending connections
    209 # and in the worst case the browser could make multiple connections and exceed
    210 # this backlog - causing the server to drop requests.  Using ThreadingMixIn
    211 # helps reduce the chance this will happen.
    212 # There were apparently some problems using this Mixin with Python 2.5, but we
    213 # are no longer using anything older than 2.6.
    214 class Server(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
    215 
    216   def Configure(
    217     self, file_mapping, redirect_mapping, extensions_mapping, allow_404,
    218     bandwidth, listener, serving_dirs=[]):
    219     self.file_mapping = file_mapping
    220     self.redirect_mapping = redirect_mapping
    221     self.extensions_mapping.update(extensions_mapping)
    222     self.allow_404 = allow_404
    223     self.bandwidth = bandwidth
    224     self.listener = listener
    225     self.rpc_lock = threading.Lock()
    226     self.serving_dirs = serving_dirs
    227 
    228   def TestingBegun(self, timeout):
    229     self.test_in_progress = True
    230     # self.timeout does not affect Python 2.5.
    231     self.timeout = timeout
    232     self.ResetTimeout()
    233     self.JavaScriptIsAlive()
    234     # Have we seen any requests from the browser?
    235     self.received_request = False
    236 
    237   def ResetTimeout(self):
    238     self.last_activity = time.time()
    239     self.received_request = True
    240 
    241   def JavaScriptIsAlive(self):
    242     self.last_js_activity = time.time()
    243 
    244   def TimeSinceJSHeartbeat(self):
    245     return time.time() - self.last_js_activity
    246 
    247   def TestingEnded(self):
    248     self.test_in_progress = False
    249 
    250   def TimedOut(self, total_time):
    251     return (total_time >= 0.0 and
    252             (time.time() - self.last_activity) >= total_time)
    253 
    254 
    255 def Create(host, port):
    256   server = Server((host, port), RequestHandler)
    257   server.extensions_mapping = mimetypes.types_map.copy()
    258   server.extensions_mapping.update({
    259     '': 'application/octet-stream' # Default
    260   })
    261   return server
    262