#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import BaseHTTPServer
import imp
import logging
import multiprocessing
import optparse
import os
import SimpleHTTPServer  # pylint: disable=W0611
import socket
import sys
import time
import urlparse

if sys.version_info < (2, 6, 0):
  sys.stderr.write("python 2.6 or later is required run this script\n")
  sys.exit(1)


SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
NACL_SDK_ROOT = os.path.dirname(SCRIPT_DIR)


# We only run from the examples directory so that not too much is exposed
# via this HTTP server.  Everything in the directory is served, so there should
# never be anything potentially sensitive in the serving directory, especially
# if the machine might be a multi-user machine and not all users are trusted.
# We only serve via the loopback interface.
def SanityCheckDirectory(dirname):
  abs_serve_dir = os.path.abspath(dirname)

  # Verify we don't serve anywhere above NACL_SDK_ROOT.
  if abs_serve_dir[:len(NACL_SDK_ROOT)] == NACL_SDK_ROOT:
    return
  logging.error('For security, httpd.py should only be run from within the')
  logging.error('example directory tree.')
  logging.error('Attempting to serve from %s.' % abs_serve_dir)
  logging.error('Run with --no_dir_check to bypass this check.')
  sys.exit(1)


class PluggableHTTPServer(BaseHTTPServer.HTTPServer):
  def __init__(self, *args, **kwargs):
    BaseHTTPServer.HTTPServer.__init__(self, *args)
    self.serve_dir = kwargs.get('serve_dir', '.')
    self.test_mode = kwargs.get('test_mode', False)
    self.delegate_map = {}
    self.running = True
    self.result = 0

  def Shutdown(self, result=0):
    self.running = False
    self.result = result


class PluggableHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  def _FindDelegateAtPath(self, dirname):
    # First check the cache...
    logging.debug('Looking for cached delegate in %s...' % dirname)
    handler_script = os.path.join(dirname, 'handler.py')

    if dirname in self.server.delegate_map:
      result = self.server.delegate_map[dirname]
      if result is None:
        logging.debug('Found None.')
      else:
        logging.debug('Found delegate.')
      return result

    # Don't have one yet, look for one.
    delegate = None
    logging.debug('Testing file %s for existence...' % handler_script)
    if os.path.exists(handler_script):
      logging.debug(
          'File %s exists, looking for HTTPRequestHandlerDelegate.' %
          handler_script)

      module = imp.load_source('handler', handler_script)
      delegate_class = getattr(module, 'HTTPRequestHandlerDelegate', None)
      delegate = delegate_class()
      if not delegate:
        logging.warn(
            'Unable to find symbol HTTPRequestHandlerDelegate in module %s.' %
            handler_script)

    return delegate

  def _FindDelegateForURLRecurse(self, cur_dir, abs_root):
    delegate = self._FindDelegateAtPath(cur_dir)
    if not delegate:
      # Didn't find it, try the parent directory, but stop if this is the server
      # root.
      if cur_dir != abs_root:
        parent_dir = os.path.dirname(cur_dir)
        delegate = self._FindDelegateForURLRecurse(parent_dir, abs_root)

    logging.debug('Adding delegate to cache for %s.' % cur_dir)
    self.server.delegate_map[cur_dir] = delegate
    return delegate

  def _FindDelegateForURL(self, url_path):
    path = self.translate_path(url_path)
    if os.path.isdir(path):
      dirname = path
    else:
      dirname = os.path.dirname(path)

    abs_serve_dir = os.path.abspath(self.server.serve_dir)
    delegate = self._FindDelegateForURLRecurse(dirname, abs_serve_dir)
    if not delegate:
      logging.info('No handler found for path %s. Using default.' % url_path)
    return delegate

  def _SendNothingAndDie(self, result=0):
    self.send_response(200, 'OK')
    self.send_header('Content-type', 'text/html')
    self.send_header('Content-length', '0')
    self.end_headers()
    self.server.Shutdown(result)

  def send_head(self):
    delegate = self._FindDelegateForURL(self.path)
    if delegate:
      return delegate.send_head(self)
    return self.base_send_head()

  def base_send_head(self):
    return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)

  def do_GET(self):
    # TODO(binji): pyauto tests use the ?quit=1 method to kill the server.
    # Remove this when we kill the pyauto tests.
    _, _, _, query, _ = urlparse.urlsplit(self.path)
    if query:
      params = urlparse.parse_qs(query)
      if '1' in params.get('quit', []):
        self._SendNothingAndDie()
        return

    delegate = self._FindDelegateForURL(self.path)
    if delegate:
      return delegate.do_GET(self)
    return self.base_do_GET()

  def base_do_GET(self):
    return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)

  def do_POST(self):
    delegate = self._FindDelegateForURL(self.path)
    if delegate:
      return delegate.do_POST(self)
    return self.base_do_POST()

  def base_do_POST(self):
    if self.server.test_mode:
      if self.path == '/ok':
        self._SendNothingAndDie(0)
      elif self.path == '/fail':
        self._SendNothingAndDie(1)


class LocalHTTPServer(object):
  """Class to start a local HTTP server as a child process."""

  def __init__(self, dirname, port, test_mode):
    parent_conn, child_conn = multiprocessing.Pipe()
    self.process = multiprocessing.Process(
        target=_HTTPServerProcess,
        args=(child_conn, dirname, port, {
          'serve_dir': dirname,
          'test_mode': test_mode,
        }))
    self.process.start()
    if parent_conn.poll(10):  # wait 10 seconds
      self.port = parent_conn.recv()
    else:
      raise Exception('Unable to launch HTTP server.')

    self.conn = parent_conn

  def ServeForever(self):
    """Serve until the child HTTP process tells us to stop.

    Returns:
      The result from the child (as an errorcode), or 0 if the server was
      killed not by the child (by KeyboardInterrupt for example).
    """
    child_result = 0
    try:
      # Block on this pipe, waiting for a response from the child process.
      child_result = self.conn.recv()
    except KeyboardInterrupt:
      pass
    finally:
      self.Shutdown()
    return child_result

  def ServeUntilSubprocessDies(self, process):
    """Serve until the child HTTP process tells us to stop or |subprocess| dies.

    Returns:
      The result from the child (as an errorcode), or 0 if |subprocess| died,
      or the server was killed some other way (by KeyboardInterrupt for
      example).
    """
    child_result = 0
    try:
      while True:
        if process.poll() is not None:
          child_result = 0
          break
        if self.conn.poll():
          child_result = self.conn.recv()
          break
        time.sleep(0)
    except KeyboardInterrupt:
      pass
    finally:
      self.Shutdown()
    return child_result

  def Shutdown(self):
    """Send a message to the child HTTP server process and wait for it to
        finish."""
    self.conn.send(False)
    self.process.join()

  def GetURL(self, rel_url):
    """Get the full url for a file on the local HTTP server.

    Args:
      rel_url: A URL fragment to convert to a full URL. For example,
          GetURL('foobar.baz') -> 'http://localhost:1234/foobar.baz'
    """
    return 'http://localhost:%d/%s' % (self.port, rel_url)


def _HTTPServerProcess(conn, dirname, port, server_kwargs):
  """Run a local httpserver with the given port or an ephemeral port.

  This function assumes it is run as a child process using multiprocessing.

  Args:
    conn: A connection to the parent process. The child process sends
        the local port, and waits for a message from the parent to
        stop serving. It also sends a "result" back to the parent -- this can
        be used to allow a client-side test to notify the server of results.
    dirname: The directory to serve. All files are accessible through
       http://localhost:<port>/path/to/filename.
    port: The port to serve on. If 0, an ephemeral port will be chosen.
    server_kwargs: A dict that will be passed as kwargs to the server.
  """
  try:
    os.chdir(dirname)
    httpd = PluggableHTTPServer(('', port), PluggableHTTPRequestHandler,
                                **server_kwargs)
  except socket.error as e:
    sys.stderr.write('Error creating HTTPServer: %s\n' % e)
    sys.exit(1)

  try:
    conn.send(httpd.server_address[1])  # the chosen port number
    httpd.timeout = 0.5  # seconds
    while httpd.running:
      # Flush output for MSVS Add-In.
      sys.stdout.flush()
      sys.stderr.flush()
      httpd.handle_request()
      if conn.poll():
        httpd.running = conn.recv()
  except KeyboardInterrupt:
    pass
  finally:
    conn.send(httpd.result)
    conn.close()


def main(args):
  parser = optparse.OptionParser()
  parser.add_option('-C', '--serve-dir',
      help='Serve files out of this directory.',
      dest='serve_dir', default=os.path.abspath('.'))
  parser.add_option('-p', '--port',
      help='Run server on this port.',
      dest='port', default=5103)
  parser.add_option('--no_dir_check',
      help='No check to ensure serving from safe directory.',
      dest='do_safe_check', action='store_false', default=True)
  parser.add_option('--test-mode',
      help='Listen for posts to /ok or /fail and shut down the server with '
          ' errorcodes 0 and 1 respectively.',
      dest='test_mode', action='store_true')
  options, args = parser.parse_args(args)
  if options.do_safe_check:
    SanityCheckDirectory(options.serve_dir)

  server = LocalHTTPServer(options.serve_dir, int(options.port),
                           options.test_mode)

  # Serve until the client tells us to stop. When it does, it will give us an
  # errorcode.
  print 'Serving %s on %s...' % (options.serve_dir, server.GetURL(''))
  return server.ServeForever()

if __name__ == '__main__':
  sys.exit(main(sys.argv[1:]))
