"""
msHTTPServer.py - stand-alone single-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)

$Id: msHTTPServer.py,v 1.53 2002/01/11 12:46:47 michael Exp $

If module the M2Crypto is present it's possible to start
a SSL-enabled web server.
"""

__version__ = '0.1.4'

import sys,os,string,time,ipadr,SocketServer,socket
       
try:
  from os import getuid
except ImportError:
  def getuid():
    return None


class HTTPServer(SocketServer.TCPServer):
  """
  Base class for a HTTP server.
  """
  runMode=0  
  def __init__(self, server_address, RequestHandlerClass):
    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)

  def handle_error(self,request,client_address):
      """
      Generic low-level handler for exceptions
      """
      import traceback
      exc_obj,exc_value,exc_traceback = sys.exc_info()
      if isinstance(exc_value,KeyboardInterrupt):
        raise KeyboardInterrupt
      elif isinstance(exc_value,IOError) and exc_value.errno==32:
        # The user has aborted access and the remote
        # connection end-point is not available anymore
        Msg = 'IOError %s: User interrupted connection.' % (
          repr(exc_value.errno)
        )
        outf = sys.stdout
      elif isinstance(exc_value,socket.error):
        Msg = 'Socket error: %s' % (repr(exc_value.args))
        outf = sys.stderr
      else:
        Msg = 'Unhandled exception:\n'+''.join(traceback.format_exception(exc_obj,exc_value,exc_traceback,20))
        outf = sys.stderr
      logentry=['%s %s:%d %s\n' % (
        time.strftime(
          '%Y-%m-%dT%H:%M:%SZ',time.gmtime(time.time())
        ),
        client_address[0],client_address[1],
        Msg,
      )]
      outf.write(os.linesep.join(logentry))
      # Avoid memory leaks
      exc_obj=None;exc_value=None;exc_traceback=None
      del exc_obj;del exc_value;del exc_traceback

try:
  import threading
except ImportError:
  ThreadingHTTPServer = HTTPServer
else:

  class HTTPThread(threading.Thread):
    """Thread class for HTTP handler thread"""
    def __init__(self,serverInstance,func,request,client_address,handle_error):
      self.serverInstance = serverInstance
      self._func=func
      self._request=request
      self._client_address=client_address
      self._handle_error=handle_error
      self.started=time.time()
      threading.Thread.__init__(self)
      self.setName(
        self.__class__.__name__+self.getName()[6:]
      )
    def __repr__(self):
      return '%s:%d - %s started %s' % (
        self._client_address[0],self._client_address[1],
        self.getName(),
        time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime(self.started)),
      )
    def isAlive(self):
      """Check if remote end is still alive by sending empty string"""
      try:
        bytessent = self._request.send("")
      except IOError:
        return 0
      else:
        return 1
    def run(self):
      try:
        try:
          apply(
            self._func,
            (self._request,self._client_address)
          )
        except:
          apply(
            self._handle_error,
            (self._request,self._client_address)
          )
          raise
      finally:
        self.serverInstance.close_request(self._request)
      del self.serverInstance

  class ThreadingHTTPServer(HTTPServer):
    runMode=1
    def process_request(self, request, client_address):
      """Start a new thread to process the request."""
      t = HTTPThread(
        self,self.finish_request,request,client_address,self.handle_error
      )
      t.start()
    if SocketServer.__version__<='0.3':
      def close_request(self,request):
        """Work around bug in SocketServer.BaseServer.handle_request()
        of Python 2.1"""
        return


#if os.name == 'posix':
#  class ForkingHTTPServer(
#    SocketServer.ForkingMixIn,
#    HTTPServer
#  ):
#    runMode=2
#else:
#  ForkingHTTPServer = HTTPServer

ForkingHTTPServer = HTTPServer

# Define classes for SSL-capable servers only if M2Crypto is present
try:
  import M2Crypto
except ImportError:
  M2Crypto_available = 0
else:

  M2Crypto_available = 1
  class HTTPSServer(M2Crypto.SSL.SSLServer):
    runMode=0
    def __init__(self,server_address,RequestHandlerClass,ssl_context):
      M2Crypto.SSL.SSLServer.__init__(self,server_address,RequestHandlerClass,ssl_context)
  try:
    import threading
  except ImportError:
    ThreadingHTTPServer = HTTPServer
  else:
    class ThreadingHTTPSServer(SocketServer.ThreadingMixIn,HTTPSServer):
      runMode=1
      def process_request(self, request, client_address):
        """Start a new thread to process the request."""
        t = HTTPThread(
          self,self.finish_request,request,client_address,self.handle_error
        )
        t.start()
      def close_request(self,request):
        """Work around bug in SocketServer.BaseServer.handle_request()
        of Python 2.1"""
        return
#  if os.name == 'posix':
#    class ForkingHTTPSServer(
#      SocketServer.ForkingMixIn,
#      HTTPSServer
#    ):
#      runMode=2
  ForkingHTTPSServer = HTTPSServer

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] = socket.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(
  HTTPHandlerClass,
  server_address=['',80],
  run_detached=1,
  run_threaded=0,
  ssl_enabled=0,
  uid=0,
  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(HTTPHandlerClass.server_env['DOCUMENT_ROOT'])):
    os.chdir(os.path.abspath(HTTPHandlerClass.server_env['DOCUMENT_ROOT']))
  else:
    print 'Warning: document_root %s does not exist.' % (
      HTTPHandlerClass.server_env['DOCUMENT_ROOT']
    )
  print 'Accepted IP address ranges: %s' % (
    string.join(
      map(
        ipadr.AddrMask_Tuple2Str,
        HTTPHandlerClass.access_allowed
      ),
      ','
    )
  )

  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.load_client_CA(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:
      ServerInstance = ThreadingHTTPSServer(server_address,HTTPHandlerClass,ssl_ctx)
    else:
      ServerInstance = ThreadingHTTPServer(server_address,HTTPHandlerClass)
  else:
    if ssl_enabled:
      ServerInstance = ForkingHTTPSServer(server_address,HTTPHandlerClass,ssl_ctx)
    else:
      ServerInstance = ForkingHTTPServer(server_address,HTTPHandlerClass)

  print 'Running mode: %s' % (
    {
      0:'single-threaded',1:'multi-threaded',2:'forking'
    }[ServerInstance.runMode]
  )

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

  # Set the server port
  ServerInstance.server_port = ServerInstance.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)

  # Write startup info to stderr
  sys.stderr.write(
    'Started web server on %s://%s:%s%s\n' % (
      {0:'http',1:'https'}[ssl_enabled],
      ServerInstance.server_name,
      ServerInstance.server_port,
      HTTPHandlerClass.script_name,
    )
  )

  print """
Point your favourite browser to

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

to access the web application.""" % (
  {0:'http',1:'https'}[ssl_enabled],
  ServerInstance.server_name,
  ServerInstance.server_port,
  HTTPHandlerClass.script_name
)
  if run_detached:
    sys.stdout = HTTPHandlerClass.debug_log
    sys.stderr = HTTPHandlerClass.error_log
    try:
      ServerInstance.serve_forever()
    finally:
      ServerInstance.socket.close()
  else:
    try:
      ServerInstance.serve_forever()
    except KeyboardInterrupt:
      print 'Shutting down web server'
      ServerInstance.socket.close()

  return # RunServer()
