########################################################################
# msHTTPServer.py
# Module for starting a stand-alone web application server
# (c) by Michael Stroeder <michael@stroeder.com>
########################################################################
# This module is distributed under the terms of the
# GPL (GNU GENERAL PUBLIC LICENSE) Version 2
# (see http://www.gnu.org/copyleft/gpl.html)
########################################################################

import sys, os, string, msbase, ipadr, \
       SocketServer,socket,SimpleHTTPServer,urllib
from socket import gethostname,gethostbyaddr,gethostbyname
try:
  from os import getuid
except ImportError:
  def getuid():
    return None

class MyHTTPServer(SocketServer.TCPServer):
  
  def __init__(self, server_address, RequestHandlerClass):
    self.access_log = sys.stdout
    self.error_log = sys.stderr
    SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass)

  def server_bind(self):
    """Override server_bind to set socket options."""
    self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    SocketServer.TCPServer.server_bind(self)

class MyThreadingHTTPServer(
  SocketServer.ThreadingMixIn,
  MyHTTPServer
):
  pass

if os.name == 'posix':
  class MyForkingHTTPServer(
    SocketServer.ForkingMixIn,
    MyHTTPServer
  ):
    pass
else:
  MyForkingHTTPServer = MyHTTPServer

# Define classes for SSL-capable servers only if M2Crypto is present
try:

  import M2Crypto
  M2Crypto_available = 1

  class MyHTTPSServer(M2Crypto.SSL.SSLServer):
    def __init__(self, server_address, RequestHandlerClass, ssl_context):
      self.access_log = sys.stdout
      self.error_log = sys.stderr
      M2Crypto.SSL.SSLServer.__init__(self, server_address, RequestHandlerClass, ssl_context)

  class MyThreadingHTTPSServer(
    SocketServer.ThreadingMixIn,
    MyHTTPSServer
  ):
    pass

  class MyForkingHTTPSServer(
    SocketServer.ForkingMixIn,
    MyHTTPSServer
  ):
    pass

except ImportError:
  M2Crypto_available = 0


# Sub-class for serving HTTP requests
class HTTPHandlerClass(SimpleHTTPServer.SimpleHTTPRequestHandler):

  # Make sure the connection is closed
  def finish(self):
    SimpleHTTPServer.SimpleHTTPRequestHandler.finish(self)
    self.connection.close()

  # Log error messages
  def log_error(self, format, *args):
    self.error_log.write("%s - - [%s] %s\n" %
                     (self.address_string(),
                      self.log_date_time_string(),
                      format%args))
    self.error_log.flush()

  # Log all access messages
  def log_message(self, format, *args):
    self.access_log.write("%s - - [%s] %s\n" %
                     (self.address_string(),
                      self.log_date_time_string(),
                      format%args))
    self.access_log.flush()

  # Return usual CGI-BIN environment of current request as dictionary
  def get_http_env(self):
    dir, rest = '', self.path[1:]
    i = string.find(rest, '?')
    if i >= 0:
        rest, query = rest[:i], rest[i+1:]
    else:
        query = ''
    i = string.find(rest, '/')
    if i >= 0:
        script, rest = rest[:i], rest[i:]
    else:
        script, rest = rest, ''

    # env is the connection-dependent environment
    env = {}
    env.update(self.server_env)
    env['SERVER_NAME'] = self.server.server_name
    env['SERVER_PORT'] = str(self.server.server_port)
    if self.headers.typeheader is None:
      env['CONTENT_TYPE'] = self.headers.type
    else:
      env['CONTENT_TYPE'] = self.headers.typeheader
    env['REQUEST_METHOD'] = self.command
    env['SCRIPT_NAME'] = self.script_name
    env['PATH_INFO'] = urllib.unquote(rest)
    env['QUERY_STRING'] = query
    env['REMOTE_ADDR'] = self.client_address[0]
    env['REMOTE_PORT'] = self.client_address[1]
    env['SCRIPT_FILENAME'] = sys.argv[0]
    env['REQUEST_URI'] = self.path[1:]
    for envitem in [
      ('Content-length','CONTENT_LENGTH'),
      ('User-Agent','HTTP_USER_AGENT'),
      ('Accept','HTTP_ACCEPT'),
      ('Accept-Charset','HTTP_ACCEPT_CHARSET'),
      ('Accept-Encoding','HTTP_ACCEPT_ENCODING'),
      ('Accept-Language','HTTP_ACCEPT_LANGUAGE'),
      ('Referer','HTTP_REFERER'),
      ('Connection','HTTP_CONNECTION'),
    ]:
      http_header_value = self.headers.getheader(envitem[0])
      if http_header_value:
        env[envitem[1]] = http_header_value

    # SERVER_SIGNATURE is built with string template and all connection data
    disp_env = msbase.DefaultDict('')
    disp_env.update(env)
    env['SERVER_SIGNATURE'] = self.server_env['SERVER_SIGNATURE'] % disp_env

    return env

  # Checks if remote IP address is allowed to access
  def check_IPAdress(self):
    return ipadr.MatchIPAdrList(self.client_address[0],self.access_allowed)

  # Send HTTP response 403 - access denied
  def Send403Error(self,http_env):
    disp_env = msbase.DefaultDict('')
    disp_env.update(http_env)
    self.send_response(403, "access denied")
    self.wfile.write("""Content-type: text/html

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
  <head>
    <title>403 Access denied</title>
  </head>
  <body>
    <h1>Access denied</h1>
    Your are not allowed to access URL
    <blockquote>
      <strong>%(REQUEST_URI)s</strong>
    </blockquote>
    from IP address
    <blockquote>
      <strong>%(REMOTE_ADDR)s</strong>.
    </blockquote>
    %(SERVER_SIGNATURE)s
  </body>
</html>
""" % disp_env)

  # Determine if the web application itself is accessed.
  def is_webapp(self):
    return self.path[:9]==self.script_name

  # Handle HTTP-POST requests
  def do_POST(self):
    http_env = self.get_http_env()
    if self.check_IPAdress():
      if self.is_webapp():
	self.run_app(http_env)
      else:
	self.send_error(501, "Can only POST to web application.")
    else:
      self.Send403Error(http_env)

  # Handle all HTTP-GET and -HEAD requests.
  def send_head(self):
    http_env = self.get_http_env()
    if self.check_IPAdress():
      if self.is_webapp():
	return self.run_app(http_env)
      else:
	return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
    else:
      return self.Send403Error(http_env)

  # Start application itself.
  def run_app(self,http_env):
    # Send start of HTTP response header
    self.send_response(200, "%s output follows" % (self.server_software))
    return

def PrintUsage(ErrorMsg=''):
  print """
usage: %s [options]

Options:

-h or -?
    Print out this message

-d on/off
    demon mode (detach from console)
    Default: on

-t on/off
    Run multi-threaded HTTP server.
    If starting multi-threaded fails the script falls
    backs to running a single-threaded HTTP server.
    Default: on

-s on/off
    Have SSL on/off.

-l [hostname:]port
    Listen to hostname:port. Either hostname:port or
    port is allowed.
    Default: your hostname:1760

-u numeric uid or username
    Switch to given UID or username after binding
    to socket.
    Default: current UID or nobody if started as root.

""" % (sys.argv[0])
  if ErrorMsg:
    print '*** Error: %s' % (ErrorMsg)
  sys.exit(1)


# Get the server startup parameters from defaults and command-line options
def GetCommandlineParams(
  server_address=['',80],
  run_detached=1,
  run_threaded=0,
  ssl_enabled=0,
  uidparam=''
):

  current_uid = getuid()
  if not current_uid is None:
    try:
      import pwd
    except ImportError:
      nopwd = 1
    else:
      if os.getuid()==0 or os.geteuid()==0:
	uid = pwd.getpwnam(uidparam)[2]
      else:
	uid = current_uid
      nopwd = 0
  else:
    uid = current_uid

  # Get startup arguments from command-line options
  import getopt

  try:
    optlist, args=getopt.getopt(sys.argv[1:],"?hs:d:t:u:l:")
  except getopt.error,e:
    PrintUsage(str(e))
    sys.exit(1)

  for k,v in optlist:

    if k=="-d":
      flag = string.lower(v)
      if flag in ['on','off']:
        run_detached = (flag=='on') and (os.name=='posix')
      else:
	PrintUsage('Detach option (option -d) must be on or off.')

    if k=="-u":
      try:
	uid = int(v)
      except ValueError:
        if current_uid is None:
          print 'Changing UID is not available on this system. Aborting.'
	  sys.exit(1)
	if nopwd:
          print 'Module pwd is not available on this system. Aborting.'
	  sys.exit(1)
	try:
	  uid = pwd.getpwnam(v)[2]
	except KeyError:
          PrintUsage('Not a valid user name (option -o).')

    if k=="-l":
      try:
	adr = map(string.strip,string.split(v,':'))
	if len(adr)==2:
	  if adr[0]:
	    server_address[0] = gethostbyname(adr[0])
	  if adr[1]:
	    server_address[1] = string.atoi(adr[1])
	elif len(adr)==1:
	  server_address[1] = string.atoi(v)
	else:
	  raise ValueError
      except ValueError:
	PrintUsage('Bind address (option -l) has invalid format.')

    if k=="-t":
      flag = string.lower(v)
      if flag in ['on','off']:
	run_threaded = flag=='on'
      else:
	PrintUsage('Threading option (option -t) must be on or off.')

    if k=="-s":
      flag = string.lower(v)
      if flag in ['on','off']:
	ssl_enabled = flag=='on'
      else:
	PrintUsage('SSL option (option -s) must be on or off.')

    if (k=="-h") or (k=="-?"):
      PrintUsage()
      
  return (
    (server_address[0],server_address[1]),
    run_detached,
    run_threaded,
    ssl_enabled,uid
  )


def RunServer(
  HTTPHandler,
  server_env,
  script_name,
  server_address=['',80],
  server_software='',
  run_detached=1,
  run_threaded=0,
  ssl_enabled=0,
  uid=0,
  document_root='',
  access_allowed=['127.0.0.1/255.0.0.0'],
  access_log='',
  error_log='',
  debug_log='',
  mime_types='/etc/mime.types',
  ssl_randfile='',
  ssl_Protocols=[],
  ssl_CertificateFile='',
  ssl_CertificateKeyFile='',
  ssl_CACertificateFile='',
  ssl_VerifyClient=0,
  ssl_VerifyDepth=1,
):

  # We will never read from stdin => close for security reasons
  sys.stdin.close()

  # Change current directory
  if os.path.isdir(os.path.abspath(document_root)):
    os.chdir(os.path.abspath(document_root))
  else:
    print 'Warning: document_root %s does not exist.' % document_root

  HTTPHandler.server_software  = server_software
  HTTPHandler.script_name      = script_name
  HTTPHandler.server_env       = server_env
  HTTPHandler.access_allowed   = map(ipadr.AddrMask_Str2Tuple,access_allowed)
  print 'Accepted IP address ranges: %s' % (string.join(access_allowed,','))

  # Open log files if running detached
  if run_detached:
    HTTPHandler.access_log = open(access_log,'a',1)
    HTTPHandler.error_log = open(error_log,'a',1)
    HTTPHandler.debug_log = open(debug_log,'a',1)
  else:
    HTTPHandler.access_log = sys.stdout
    HTTPHandler.error_log = sys.stderr

  # MIME-mapping
  if mime_types and os.path.isfile(mime_types):
    # Read mapping from file
    print 'Read MIME-type mapping from file %s.' % (mime_types)
    import mimetypes
    HTTPHandler.extensions_map = mimetypes.read_mime_types(mime_types)
    HTTPHandler.extensions_map[''] = 'text/plain' # Default, *must* be present
  else:
    # Define very simple default mapping suitable for our needs
    HTTPHandler.extensions_map = {
      '': 'text/plain',   # Default, *must* be present
      '.html': 'text/html',
      '.htm': 'text/html',
      '.gif': 'image/gif',
      '.jpg': 'image/jpeg',
      '.jpeg': 'image/jpeg',
      '.css': 'text/css',
    }

  if ssl_enabled:

    if not M2Crypto_available:
      PrintUsage('SSL needs installed module M2Crypto\n(available from http://www.post1.com/home/ngps/m2/).')

    if ssl_randfile:
      print 'Seeding random generator with %s' % (ssl_randfile)
      M2Crypto.Rand.load_file(ssl_randfile, -1)
    # build SSL context
    ssl_ctx=M2Crypto.SSL.Context(ssl_Protocols[0])
    ssl_ctx.load_cert(ssl_CertificateFile,ssl_CertificateKeyFile)
    ssl_ctx.load_verify_location(ssl_CACertificateFile)
    ssl_ctx.set_verify(ssl_VerifyClient,ssl_VerifyDepth)
    ssl_ctx.set_session_id_ctx('%s' % (os.getpid()))
    # We do not receive SSL connection info with empty callback
    ssl_ctx.set_info_callback()

  if run_threaded:
    # Try running multi-threaded
    if ssl_enabled:
      print server_address
      ServerClass = MyThreadingHTTPSServer(server_address,HTTPHandler,ssl_ctx)
    else:
      ServerClass = MyThreadingHTTPServer(server_address,HTTPHandler)
    print 'Running mode: multi-threaded'
  else:
    if ssl_enabled:
      ServerClass = MyForkingHTTPSServer(server_address,HTTPHandler,ssl_ctx)
    else:
      ServerClass = MyForkingHTTPServer(server_address,HTTPHandler)
    print 'Running mode: forking'

  # Set the server name
  if ServerClass.server_address[0]:
    try:
      ServerClass.server_name = gethostbyaddr(ServerClass.server_address[0])[0]
    except socket.error:
      ServerClass.server_name = ServerClass.server_address[0]
  else:
    ServerClass.server_name = gethostbyaddr(gethostbyname(gethostname()))[0]

  # Set the server port
  ServerClass.server_port = ServerClass.server_address[1]

  print 'SSL: %s' % ({0:'disabled',1:'enabled'}[ssl_enabled])

  if (not uid is None) and (uid!=os.getuid()):
    try:
      os.setuid(uid)
      print 'Changed to UID %d.' % (uid)
    except os.error:
      print 'Error changing to UID %d! Aborting.' % (uid)
      sys.exit(1)

  print """
Point your favourite browser to

%s://%s:%s%s

to access the web gateway.""" % (
{0:'http',1:'https'}[ssl_enabled],
ServerClass.server_name,
ServerClass.server_port,
script_name
)

  if run_detached:
    sys.stdout = sys.stderr = HTTPHandler.debug_log
    try:
      ServerClass.serve_forever()
    except:
      pass
  else:
    try:
      ServerClass.serve_forever()
    except KeyboardInterrupt:
      print 'Shutting down web server'
      ServerClass.socket.close()

  sys.exit(0)

def ServerStartup(
  HTTPHandler,
  server_env,
  script_name,
  server_address=['',80],
  server_software='',
  run_detached=1,
  run_threaded=0,
  ssl_enabled=0,
  uid=0,
  document_root='',
  access_allowed=['127.0.0.1/255.0.0.0'],
  access_log='',
  error_log='',
  debug_log='',
  mime_types='/etc/mime.types',
  ssl_randfile='',
  ssl_Protocols=[],
  ssl_CertificateFile='',
  ssl_CertificateKeyFile='',
  ssl_CACertificateFile='',
  ssl_VerifyClient=0,
  ssl_VerifyDepth=1,
):

  server_address,run_detached,run_threaded,ssl_enabled,uid = GetCommandlineParams(
    server_address,run_detached,run_threaded,ssl_enabled,uid)

  if (os.name == 'posix') and run_detached:
    if os.fork():
      sys.exit(0)
    else:
      os.setsid()
      RunServer(
	HTTPHandler,
	server_env,
	script_name,
	server_address,
        server_software,
	run_detached,
	run_threaded,
	ssl_enabled,
	uid,
	document_root,
	access_allowed,
	access_log,
	error_log,
        debug_log,
	mime_types,
	ssl_randfile,
	ssl_Protocols,
	ssl_CertificateFile,
	ssl_CertificateKeyFile,
	ssl_CACertificateFile,
	ssl_VerifyClient,
	ssl_VerifyDepth,
      )
      
  else:
    RunServer(
      HTTPHandler,
      server_env,
      script_name,
      server_address,
      server_software,
      run_detached,
      run_threaded,
      ssl_enabled,
      uid,
      document_root,
      access_allowed,
      access_log,
      error_log,
      debug_log,
      mime_types,
      ssl_randfile,
      ssl_Protocols,
      ssl_CertificateFile,
      ssl_CertificateKeyFile,
      ssl_CACertificateFile,
      ssl_VerifyClient,
      ssl_VerifyDepth,
    )
