Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 import BaseHTTPServer
      7 import imp
      8 import logging
      9 import multiprocessing
     10 import optparse
     11 import os
     12 import SimpleHTTPServer  # pylint: disable=W0611
     13 import socket
     14 import sys
     15 import time
     16 import urlparse
     17 
     18 if sys.version_info < (2, 6, 0):
     19   sys.stderr.write("python 2.6 or later is required run this script\n")
     20   sys.exit(1)
     21 
     22 
     23 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
     24 NACL_SDK_ROOT = os.path.dirname(SCRIPT_DIR)
     25 
     26 
     27 # We only run from the examples directory so that not too much is exposed
     28 # via this HTTP server.  Everything in the directory is served, so there should
     29 # never be anything potentially sensitive in the serving directory, especially
     30 # if the machine might be a multi-user machine and not all users are trusted.
     31 # We only serve via the loopback interface.
     32 def SanityCheckDirectory(dirname):
     33   abs_serve_dir = os.path.abspath(dirname)
     34 
     35   # Verify we don't serve anywhere above NACL_SDK_ROOT.
     36   if abs_serve_dir[:len(NACL_SDK_ROOT)] == NACL_SDK_ROOT:
     37     return
     38   logging.error('For security, httpd.py should only be run from within the')
     39   logging.error('example directory tree.')
     40   logging.error('Attempting to serve from %s.' % abs_serve_dir)
     41   logging.error('Run with --no_dir_check to bypass this check.')
     42   sys.exit(1)
     43 
     44 
     45 class PluggableHTTPServer(BaseHTTPServer.HTTPServer):
     46   def __init__(self, *args, **kwargs):
     47     BaseHTTPServer.HTTPServer.__init__(self, *args)
     48     self.serve_dir = kwargs.get('serve_dir', '.')
     49     self.test_mode = kwargs.get('test_mode', False)
     50     self.delegate_map = {}
     51     self.running = True
     52     self.result = 0
     53 
     54   def Shutdown(self, result=0):
     55     self.running = False
     56     self.result = result
     57 
     58 
     59 class PluggableHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
     60   def _FindDelegateAtPath(self, dirname):
     61     # First check the cache...
     62     logging.debug('Looking for cached delegate in %s...' % dirname)
     63     handler_script = os.path.join(dirname, 'handler.py')
     64 
     65     if dirname in self.server.delegate_map:
     66       result = self.server.delegate_map[dirname]
     67       if result is None:
     68         logging.debug('Found None.')
     69       else:
     70         logging.debug('Found delegate.')
     71       return result
     72 
     73     # Don't have one yet, look for one.
     74     delegate = None
     75     logging.debug('Testing file %s for existence...' % handler_script)
     76     if os.path.exists(handler_script):
     77       logging.debug(
     78           'File %s exists, looking for HTTPRequestHandlerDelegate.' %
     79           handler_script)
     80 
     81       module = imp.load_source('handler', handler_script)
     82       delegate_class = getattr(module, 'HTTPRequestHandlerDelegate', None)
     83       delegate = delegate_class()
     84       if not delegate:
     85         logging.warn(
     86             'Unable to find symbol HTTPRequestHandlerDelegate in module %s.' %
     87             handler_script)
     88 
     89     return delegate
     90 
     91   def _FindDelegateForURLRecurse(self, cur_dir, abs_root):
     92     delegate = self._FindDelegateAtPath(cur_dir)
     93     if not delegate:
     94       # Didn't find it, try the parent directory, but stop if this is the server
     95       # root.
     96       if cur_dir != abs_root:
     97         parent_dir = os.path.dirname(cur_dir)
     98         delegate = self._FindDelegateForURLRecurse(parent_dir, abs_root)
     99 
    100     logging.debug('Adding delegate to cache for %s.' % cur_dir)
    101     self.server.delegate_map[cur_dir] = delegate
    102     return delegate
    103 
    104   def _FindDelegateForURL(self, url_path):
    105     path = self.translate_path(url_path)
    106     if os.path.isdir(path):
    107       dirname = path
    108     else:
    109       dirname = os.path.dirname(path)
    110 
    111     abs_serve_dir = os.path.abspath(self.server.serve_dir)
    112     delegate = self._FindDelegateForURLRecurse(dirname, abs_serve_dir)
    113     if not delegate:
    114       logging.info('No handler found for path %s. Using default.' % url_path)
    115     return delegate
    116 
    117   def _SendNothingAndDie(self, result=0):
    118     self.send_response(200, 'OK')
    119     self.send_header('Content-type', 'text/html')
    120     self.send_header('Content-length', '0')
    121     self.end_headers()
    122     self.server.Shutdown(result)
    123 
    124   def send_head(self):
    125     delegate = self._FindDelegateForURL(self.path)
    126     if delegate:
    127       return delegate.send_head(self)
    128     return self.base_send_head()
    129 
    130   def base_send_head(self):
    131     return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
    132 
    133   def do_GET(self):
    134     # TODO(binji): pyauto tests use the ?quit=1 method to kill the server.
    135     # Remove this when we kill the pyauto tests.
    136     _, _, _, query, _ = urlparse.urlsplit(self.path)
    137     if query:
    138       params = urlparse.parse_qs(query)
    139       if '1' in params.get('quit', []):
    140         self._SendNothingAndDie()
    141         return
    142 
    143     delegate = self._FindDelegateForURL(self.path)
    144     if delegate:
    145       return delegate.do_GET(self)
    146     return self.base_do_GET()
    147 
    148   def base_do_GET(self):
    149     return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
    150 
    151   def do_POST(self):
    152     delegate = self._FindDelegateForURL(self.path)
    153     if delegate:
    154       return delegate.do_POST(self)
    155     return self.base_do_POST()
    156 
    157   def base_do_POST(self):
    158     if self.server.test_mode:
    159       if self.path == '/ok':
    160         self._SendNothingAndDie(0)
    161       elif self.path == '/fail':
    162         self._SendNothingAndDie(1)
    163 
    164 
    165 class LocalHTTPServer(object):
    166   """Class to start a local HTTP server as a child process."""
    167 
    168   def __init__(self, dirname, port, test_mode):
    169     parent_conn, child_conn = multiprocessing.Pipe()
    170     self.process = multiprocessing.Process(
    171         target=_HTTPServerProcess,
    172         args=(child_conn, dirname, port, {
    173           'serve_dir': dirname,
    174           'test_mode': test_mode,
    175         }))
    176     self.process.start()
    177     if parent_conn.poll(10):  # wait 10 seconds
    178       self.port = parent_conn.recv()
    179     else:
    180       raise Exception('Unable to launch HTTP server.')
    181 
    182     self.conn = parent_conn
    183 
    184   def ServeForever(self):
    185     """Serve until the child HTTP process tells us to stop.
    186 
    187     Returns:
    188       The result from the child (as an errorcode), or 0 if the server was
    189       killed not by the child (by KeyboardInterrupt for example).
    190     """
    191     child_result = 0
    192     try:
    193       # Block on this pipe, waiting for a response from the child process.
    194       child_result = self.conn.recv()
    195     except KeyboardInterrupt:
    196       pass
    197     finally:
    198       self.Shutdown()
    199     return child_result
    200 
    201   def ServeUntilSubprocessDies(self, process):
    202     """Serve until the child HTTP process tells us to stop or |subprocess| dies.
    203 
    204     Returns:
    205       The result from the child (as an errorcode), or 0 if |subprocess| died,
    206       or the server was killed some other way (by KeyboardInterrupt for
    207       example).
    208     """
    209     child_result = 0
    210     try:
    211       while True:
    212         if process.poll() is not None:
    213           child_result = 0
    214           break
    215         if self.conn.poll():
    216           child_result = self.conn.recv()
    217           break
    218         time.sleep(0)
    219     except KeyboardInterrupt:
    220       pass
    221     finally:
    222       self.Shutdown()
    223     return child_result
    224 
    225   def Shutdown(self):
    226     """Send a message to the child HTTP server process and wait for it to
    227         finish."""
    228     self.conn.send(False)
    229     self.process.join()
    230 
    231   def GetURL(self, rel_url):
    232     """Get the full url for a file on the local HTTP server.
    233 
    234     Args:
    235       rel_url: A URL fragment to convert to a full URL. For example,
    236           GetURL('foobar.baz') -> 'http://localhost:1234/foobar.baz'
    237     """
    238     return 'http://localhost:%d/%s' % (self.port, rel_url)
    239 
    240 
    241 def _HTTPServerProcess(conn, dirname, port, server_kwargs):
    242   """Run a local httpserver with the given port or an ephemeral port.
    243 
    244   This function assumes it is run as a child process using multiprocessing.
    245 
    246   Args:
    247     conn: A connection to the parent process. The child process sends
    248         the local port, and waits for a message from the parent to
    249         stop serving. It also sends a "result" back to the parent -- this can
    250         be used to allow a client-side test to notify the server of results.
    251     dirname: The directory to serve. All files are accessible through
    252        http://localhost:<port>/path/to/filename.
    253     port: The port to serve on. If 0, an ephemeral port will be chosen.
    254     server_kwargs: A dict that will be passed as kwargs to the server.
    255   """
    256   try:
    257     os.chdir(dirname)
    258     httpd = PluggableHTTPServer(('', port), PluggableHTTPRequestHandler,
    259                                 **server_kwargs)
    260   except socket.error as e:
    261     sys.stderr.write('Error creating HTTPServer: %s\n' % e)
    262     sys.exit(1)
    263 
    264   try:
    265     conn.send(httpd.server_address[1])  # the chosen port number
    266     httpd.timeout = 0.5  # seconds
    267     while httpd.running:
    268       # Flush output for MSVS Add-In.
    269       sys.stdout.flush()
    270       sys.stderr.flush()
    271       httpd.handle_request()
    272       if conn.poll():
    273         httpd.running = conn.recv()
    274   except KeyboardInterrupt:
    275     pass
    276   finally:
    277     conn.send(httpd.result)
    278     conn.close()
    279 
    280 
    281 def main(args):
    282   parser = optparse.OptionParser()
    283   parser.add_option('-C', '--serve-dir',
    284       help='Serve files out of this directory.',
    285       dest='serve_dir', default=os.path.abspath('.'))
    286   parser.add_option('-p', '--port',
    287       help='Run server on this port.',
    288       dest='port', default=5103)
    289   parser.add_option('--no_dir_check',
    290       help='No check to ensure serving from safe directory.',
    291       dest='do_safe_check', action='store_false', default=True)
    292   parser.add_option('--test-mode',
    293       help='Listen for posts to /ok or /fail and shut down the server with '
    294           ' errorcodes 0 and 1 respectively.',
    295       dest='test_mode', action='store_true')
    296   options, args = parser.parse_args(args)
    297   if options.do_safe_check:
    298     SanityCheckDirectory(options.serve_dir)
    299 
    300   server = LocalHTTPServer(options.serve_dir, int(options.port),
    301                            options.test_mode)
    302 
    303   # Serve until the client tells us to stop. When it does, it will give us an
    304   # errorcode.
    305   print 'Serving %s on %s...' % (options.serve_dir, server.GetURL(''))
    306   return server.ServeForever()
    307 
    308 if __name__ == '__main__':
    309   sys.exit(main(sys.argv[1:]))
    310