• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
6import BaseHTTPServer
7import logging
8import multiprocessing
9import optparse
10import os
11import SimpleHTTPServer  # pylint: disable=W0611
12import socket
13import sys
14import time
15import urlparse
16
17if 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
22SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
23NACL_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.
31def 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
44class 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
55class 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
75class 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
148def _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
187def 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
218if __name__ == '__main__':
219  sys.exit(main(sys.argv[1:]))
220