• 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 imp
8import logging
9import multiprocessing
10import optparse
11import os
12import SimpleHTTPServer  # pylint: disable=W0611
13import socket
14import sys
15import time
16import urlparse
17
18if 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
23SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
24NACL_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.
32def 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
45class 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
59class 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
165class 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
241def _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
281def main(args):
282  parser = optparse.OptionParser()
283  parser.add_option('-C', '--serve-dir',
284      help='Serve files out of this directory.',
285      default=os.path.abspath('.'))
286  parser.add_option('-p', '--port',
287      help='Run server on this port.', default=5103)
288  parser.add_option('--no-dir-check', '--no_dir_check',
289      help='No check to ensure serving from safe directory.',
290      dest='do_safe_check', action='store_false', default=True)
291  parser.add_option('--test-mode',
292      help='Listen for posts to /ok or /fail and shut down the server with '
293          ' errorcodes 0 and 1 respectively.',
294      action='store_true')
295
296  # To enable bash completion for this command first install optcomplete
297  # and then add this line to your .bashrc:
298  #  complete -F _optcomplete httpd.py
299  try:
300    import optcomplete
301    optcomplete.autocomplete(parser)
302  except ImportError:
303    pass
304
305  options, args = parser.parse_args(args)
306  if options.do_safe_check:
307    SanityCheckDirectory(options.serve_dir)
308
309  server = LocalHTTPServer(options.serve_dir, int(options.port),
310                           options.test_mode)
311
312  # Serve until the client tells us to stop. When it does, it will give us an
313  # errorcode.
314  print 'Serving %s on %s...' % (options.serve_dir, server.GetURL(''))
315  return server.ServeForever()
316
317if __name__ == '__main__':
318  sys.exit(main(sys.argv[1:]))
319