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 logging
      8 import multiprocessing
      9 import optparse
     10 import os
     11 import SimpleHTTPServer  # pylint: disable=W0611
     12 import socket
     13 import sys
     14 import time
     15 import urlparse
     16 
     17 if sys.version_info < (2, 6, 0):
     18   sys.stderr.write("python 2.6 or later is required run this script\n")
     19   sys.exit(1)
     20 
     21 
     22 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
     23 NACL_SDK_ROOT = os.path.dirname(SCRIPT_DIR)
     24 
     25 
     26 # We only run from the examples directory so that not too much is exposed
     27 # via this HTTP server.  Everything in the directory is served, so there should
     28 # never be anything potentially sensitive in the serving directory, especially
     29 # if the machine might be a multi-user machine and not all users are trusted.
     30 # We only serve via the loopback interface.
     31 def SanityCheckDirectory(dirname):
     32   abs_serve_dir = os.path.abspath(dirname)
     33 
     34   # Verify we don't serve anywhere above NACL_SDK_ROOT.
     35   if abs_serve_dir[:len(NACL_SDK_ROOT)] == NACL_SDK_ROOT:
     36     return
     37   logging.error('For security, httpd.py should only be run from within the')
     38   logging.error('example directory tree.')
     39   logging.error('Attempting to serve from %s.' % abs_serve_dir)
     40   logging.error('Run with --no-dir-check to bypass this check.')
     41   sys.exit(1)
     42 
     43 
     44 class HTTPServer(BaseHTTPServer.HTTPServer):
     45   def __init__(self, *args, **kwargs):
     46     BaseHTTPServer.HTTPServer.__init__(self, *args)
     47     self.running = True
     48     self.result = 0
     49 
     50   def Shutdown(self, result=0):
     51     self.running = False
     52     self.result = result
     53 
     54 
     55 class HTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
     56   def _SendNothingAndDie(self, result=0):
     57     self.send_response(200, 'OK')
     58     self.send_header('Content-type', 'text/html')
     59     self.send_header('Content-length', '0')
     60     self.end_headers()
     61     self.server.Shutdown(result)
     62 
     63   def do_GET(self):
     64     # Browsing to ?quit=1 will kill the server cleanly.
     65     _, _, _, query, _ = urlparse.urlsplit(self.path)
     66     if query:
     67       params = urlparse.parse_qs(query)
     68       if '1' in params.get('quit', []):
     69         self._SendNothingAndDie()
     70         return
     71 
     72     return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
     73 
     74 
     75 class LocalHTTPServer(object):
     76   """Class to start a local HTTP server as a child process."""
     77 
     78   def __init__(self, dirname, port):
     79     parent_conn, child_conn = multiprocessing.Pipe()
     80     self.process = multiprocessing.Process(
     81         target=_HTTPServerProcess,
     82         args=(child_conn, dirname, port, {}))
     83     self.process.start()
     84     if parent_conn.poll(10):  # wait 10 seconds
     85       self.port = parent_conn.recv()
     86     else:
     87       raise Exception('Unable to launch HTTP server.')
     88 
     89     self.conn = parent_conn
     90 
     91   def ServeForever(self):
     92     """Serve until the child HTTP process tells us to stop.
     93 
     94     Returns:
     95       The result from the child (as an errorcode), or 0 if the server was
     96       killed not by the child (by KeyboardInterrupt for example).
     97     """
     98     child_result = 0
     99     try:
    100       # Block on this pipe, waiting for a response from the child process.
    101       child_result = self.conn.recv()
    102     except KeyboardInterrupt:
    103       pass
    104     finally:
    105       self.Shutdown()
    106     return child_result
    107 
    108   def ServeUntilSubprocessDies(self, process):
    109     """Serve until the child HTTP process tells us to stop or |subprocess| dies.
    110 
    111     Returns:
    112       The result from the child (as an errorcode), or 0 if |subprocess| died,
    113       or the server was killed some other way (by KeyboardInterrupt for
    114       example).
    115     """
    116     child_result = 0
    117     try:
    118       while True:
    119         if process.poll() is not None:
    120           child_result = 0
    121           break
    122         if self.conn.poll():
    123           child_result = self.conn.recv()
    124           break
    125         time.sleep(0)
    126     except KeyboardInterrupt:
    127       pass
    128     finally:
    129       self.Shutdown()
    130     return child_result
    131 
    132   def Shutdown(self):
    133     """Send a message to the child HTTP server process and wait for it to
    134         finish."""
    135     self.conn.send(False)
    136     self.process.join()
    137 
    138   def GetURL(self, rel_url):
    139     """Get the full url for a file on the local HTTP server.
    140 
    141     Args:
    142       rel_url: A URL fragment to convert to a full URL. For example,
    143           GetURL('foobar.baz') -> 'http://localhost:1234/foobar.baz'
    144     """
    145     return 'http://localhost:%d/%s' % (self.port, rel_url)
    146 
    147 
    148 def _HTTPServerProcess(conn, dirname, port, server_kwargs):
    149   """Run a local httpserver with the given port or an ephemeral port.
    150 
    151   This function assumes it is run as a child process using multiprocessing.
    152 
    153   Args:
    154     conn: A connection to the parent process. The child process sends
    155         the local port, and waits for a message from the parent to
    156         stop serving. It also sends a "result" back to the parent -- this can
    157         be used to allow a client-side test to notify the server of results.
    158     dirname: The directory to serve. All files are accessible through
    159        http://localhost:<port>/path/to/filename.
    160     port: The port to serve on. If 0, an ephemeral port will be chosen.
    161     server_kwargs: A dict that will be passed as kwargs to the server.
    162   """
    163   try:
    164     os.chdir(dirname)
    165     httpd = HTTPServer(('', port), HTTPRequestHandler, **server_kwargs)
    166   except socket.error as e:
    167     sys.stderr.write('Error creating HTTPServer: %s\n' % e)
    168     sys.exit(1)
    169 
    170   try:
    171     conn.send(httpd.server_address[1])  # the chosen port number
    172     httpd.timeout = 0.5  # seconds
    173     while httpd.running:
    174       # Flush output for MSVS Add-In.
    175       sys.stdout.flush()
    176       sys.stderr.flush()
    177       httpd.handle_request()
    178       if conn.poll():
    179         httpd.running = conn.recv()
    180   except KeyboardInterrupt:
    181     pass
    182   finally:
    183     conn.send(httpd.result)
    184     conn.close()
    185 
    186 
    187 def main(args):
    188   parser = optparse.OptionParser()
    189   parser.add_option('-C', '--serve-dir',
    190       help='Serve files out of this directory.',
    191       default=os.path.abspath('.'))
    192   parser.add_option('-p', '--port',
    193       help='Run server on this port.', default=5103)
    194   parser.add_option('--no-dir-check', '--no_dir_check',
    195       help='No check to ensure serving from safe directory.',
    196       dest='do_safe_check', action='store_false', default=True)
    197 
    198   # To enable bash completion for this command first install optcomplete
    199   # and then add this line to your .bashrc:
    200   #  complete -F _optcomplete httpd.py
    201   try:
    202     import optcomplete
    203     optcomplete.autocomplete(parser)
    204   except ImportError:
    205     pass
    206 
    207   options, args = parser.parse_args(args)
    208   if options.do_safe_check:
    209     SanityCheckDirectory(options.serve_dir)
    210 
    211   server = LocalHTTPServer(options.serve_dir, int(options.port))
    212 
    213   # Serve until the client tells us to stop. When it does, it will give us an
    214   # errorcode.
    215   print 'Serving %s on %s...' % (options.serve_dir, server.GetURL(''))
    216   return server.ServeForever()
    217 
    218 if __name__ == '__main__':
    219   sys.exit(main(sys.argv[1:]))
    220