#!/usr/bin/env python

"""
Copyright 2008-2014, 2016 VMware, Inc.  All rights reserved. -- VMware Confidential

This module is the python vmomi server module. This hosted python managed
object implementation and serves soap requests from extern entities
"""
__author__ = "VMware, Inc"

import sys
import os

# The following imports are for main and HTTPServer
import threading
import time
import re
import io

import six
from six import BytesIO
if six.PY3:
   from http.cookies import SimpleCookie
   from http.server import BaseHTTPRequestHandler
   from http.server import HTTPServer
else:
   from Cookie import SimpleCookie
   from BaseHTTPServer import BaseHTTPRequestHandler
   from BaseHTTPServer import HTTPServer

import six.moves.socketserver as socketserver
import six.moves.queue as queue

from pyVmomi import VmomiSupport

# logging
import logging, logging.handlers
from contrib.logUtils import LoggingFactory

# SOAPAction string
_STR_SOAP_ACTION = "SOAPAction"

# SOAPAction bytes
_STR_SOAP_ACTION_BYTES = b"SOAPAction"

# Global variables
gCmdOptions = None
gCmdRemainingOptions = None
gLogPath = os.path.normpath("./")
gLogLevel = "INFO"
gSoapHandlerCls = None
UTF8='utf-8'

## Setup logging
#
def SetupLogging(options):
   """ Setup debug logging """
   LoggingFactory.ParseOptions(options)

def GetSoapHandler(stubs):
   global gSoapHandlerCls
   if gSoapHandlerCls:
      return gSoapHandlerCls(stubs)
   else:
      return SoapHandler(stubs)

def SetSoapHandlerCls(cls):
   global gSoapHandlerCls
   gSoapHandlerCls = cls

## Parse arguments
#
def ParseArguments(argv):
   """ Parse arguments """

   from optparse import OptionParser, make_option

   # Internal cmds supported by this handler
   _CMD_OPTIONS_LIST = [
      make_option("-p", "--port", dest="port", default="443", help="TCP port number"),
      make_option("-H", "--host", dest="host", default="",
                   help="Hostname, IPv4/IPv6 address of the server. By default, "
                   "server will listen on all interfaces. This is only used "
                   "when --port option is provided."),
      make_option("--unix", dest="unix", help="Unix socket path"),
      make_option("--keyfile", dest="keyfile", default=None, help="Https server private key"),
      make_option("--certfile", dest="certfile", default=None, help="Https server certificate"),
      make_option("-i", "--interactive", dest="interactive",
                   action="store_true", help="Interactive mode"),
      make_option("-f", "--file", dest="file", help="Read request from file"),
      make_option("-g", "--cgi", dest="cgi", action="store_true",
                  help="CGI mode: process a single SOAP request as a CGI script"),
      make_option("--scgi", dest="scgi", action="store_true",
                  help="SCGI mode: process multiple SOAP requests over stdio",
                  default=False),
      make_option("--rhost", dest="rhost", default=None,
                  help="Proxy mode: Remote host"),
      make_option("--rport", dest="rport", default="443",
                  help="Proxy mode: Remote port"),
      make_option("--rns", dest="rns", default="vim25/5.5",
                  help="Proxy mode: Remote namespace"),
      make_option("--rpath", dest="rpath", default="/sdk",
                  help="Proxy mode: Remote path"),
      make_option("--ignorePyMo", action="store_true", default=False, dest="ignorePyMo",
                  help="Do not load the managed objects or types under pyMo"),
      make_option("-?", action="help")
   ]
   _STR_USAGE = "%prog [options]"

   # Get command line options
   cmdParser = OptionParser(option_list=_CMD_OPTIONS_LIST,
                            usage=_STR_USAGE)
   cmdParser.allow_interspersed_args = False
   LoggingFactory.AddOptions(cmdParser)

   # Parse arguments
   (options, remainingOptions) = cmdParser.parse_args(argv)
   try:
      # optparser does not have a destroy() method in older python
      cmdParser.destroy()
   except Exception:
      pass
   del cmdParser
   return (options, remainingOptions)

def Initialize(options=None, remainingOptions=None):
   """ Initialize global options variables """
   global gCmdOptions
   gCmdOptions = options != None and options or {}
   global gCmdRemainingOptions
   gCmdRemainingOptions = remainingOptions != None and remainingOptions or {}
   ImportTypesAndManagedObjects()

def ImportTypesAndManagedObjects():
   # Import dynamic types
   # Note: Import DynTypeMgr to bring in GetDynTypeMgr(). Do not remove
   import DynTypeMgr
   import MoManager

   # Note: Import pyMo to bring in all managed objects. Do not remove
   ignorePyMo = getattr(gCmdOptions, "ignorePyMo", False)
   if not ignorePyMo:
      import pyMo

## Parse arguments
if __name__ == "__main__":
   gCmdOptions, gCmdRemainingOptions = ParseArguments(sys.argv[1:])
   SetupLogging(gCmdOptions)
   Initialize(options=gCmdOptions, remainingOptions=gCmdRemainingOptions)


## Modules that might use logging must be declared after calling SetupLogging
#
from SoapHandler import SoapHandler, ExceptionMsg

## Utility fn to log an exception with traceback
#
# @param msg Message to print
# @param err The exception
def LogException(msg, err):
   """ Log exception with stack trace """
   try:
      import traceback
      stackTraces = traceback.format_exception(sys.exc_info()[0],
                                               sys.exc_info()[1],
                                               sys.exc_info()[2])
      logging.critical(msg + ExceptionMsg(err))
      for line in stackTraces:
         logging.critical(line.rstrip())
   except Exception:
      pass

## Get version from urn string
#
# @param  urn the urn in the form of "urn:namespace/versionId"
# @return version string (namespace/versionId)
def GetVersion(urn):
   """ Get version from urn string """

   # Strip whitespaces
   urn = urn.strip()
   if urn.startswith('"'):
      urn = urn.strip('"')
   elif urn.startswith("'"):
      urn = urn.strip("'")
   if urn.startswith("urn:"):
      version = urn[4:].strip()
   else:
      version = None
   return version

## XML password filter
#
def DebugLogFilteredXML(xml):
   """ log.debug XML password filter """
   password_replacement = "<password>(not shown)</password>"
   password_pattern = "<password>[^>]*</password>|<password[^>]*/>"
   if logging.getLogger().isEnabledFor(logging.DEBUG):
      if isinstance(xml, six.binary_type):
         filtered = re.compile(password_pattern.encode(), re.IGNORECASE).sub(password_replacement.encode(), xml)
         logging.debug(filtered)
      elif isinstance(xml, six.text_type):
         filtered = re.compile(password_pattern, re.IGNORECASE).sub(password_replacement, xml)
         logging.debug(filtered)

## Message body reader. Read message body up to a specified length.
#  Also avoid reading the whole request into memory
#
class _MessageBodyReader:
   """ Read message body up to a specified length (unlimited if maxLen is None"""
   def __init__(self, rfile, maxLen=None):
      """ Constructor """
      self.rfile = rfile
      self.len = maxLen
      if maxLen is None:
         # Read until eof
         self.read = self.readAll
      else:
         self.read = self.readChunk

   def readChunk(self, bytes):
      """ Read bytes """
      if self.len > 0:
         if self.len < bytes:
            bytes = self.len
         ret = self.rfile.read(bytes)
         DebugLogFilteredXML(ret)
         self.len -= len(ret)
      else:
         ret = b""
      return ret

   def readAll(self, bytes):
      """ Read until eof """
      ret = self.rfile.read(bytes)
      DebugLogFilteredXML(ret)
      return ret


## Generic http handler class
#
class GeneralHttpHandler(BaseHTTPRequestHandler):
   """
   Http handler
   """

   # Override base class's protocol_version
   # Note: need to include accurate "Content-Length" in send_header()
   protocol_version = "HTTP/1.1"

   ## Chunked message body reader
   #
   class _ChunkedMessageBodyReader:
      """ Chunked message body reader """
      def __init__(self, rfile):
         """ Constructor """
         self.rfile = rfile
         self.chunkSize = 0
         self._StartNextChunk()

      def _StartNextChunk(self):
         """ Start next chunk """
         line = self.rfile.readline()
         chunkSize = int(line.split(b";", 1)[0], 16)
         if chunkSize == 0:
            # Remove trailer
            while True:
               line = self.rfile.readline()
               if line == b"\r\n":
                  break
         self.chunkSize = chunkSize

      def _ReadChunk(self, bytes):
         """ Read bytes from current chunk """
         assert(self.chunkSize >= bytes)
         ret = self.rfile.read(bytes)
         self.chunkSize -= bytes
         if self.chunkSize == 0:
            # Discard CRLF
            self.rfile.read(2)
            self._StartNextChunk()
         return ret

      def read(self, bytes):
         """ Read bytes """
         ret = []
         while bytes > 0 and self.chunkSize > 0:
            readSize = min(self.chunkSize, bytes)
            # Read chunk
            ret.append(self._ReadChunk(readSize))
            bytes -= readSize
         ret = b"".join(ret)
         if ret:
            DebugLogFilteredXML(ret)
         return ret

   ## Chunked message body writer
   #
   class _ChunkedMessageBodyWriter:
      """ Chunked message body reader """
      def __init__(self, wfile, chunkSize=8192):
         self.wfile = wfile
         self.chunkSize = chunkSize

         self.currChunkSize = 0
         self.chunks = []

      def _WriteChunk(self):
         if not self.wfile:
            return

         if self.currChunkSize > self.chunkSize:
            chunkSize = self.chunkSize
            leftoverBytes = self.currChunkSize - self.chunkSize
         else:
            chunkSize = self.currChunkSize
            leftoverBytes = 0

         # Write chunk header
         if six.PY3:
            chunkHeader = b"%x\r\n" % chunkSize
         else:
            chunkHeader = "%x\r\n" % chunkSize
         self.wfile.write(chunkHeader)

         chunks = self.chunks
         if leftoverBytes > 0:
            # Split the last chunk
            lastChunk = chunks.pop()
            chunks.append(lastChunk[:-leftoverBytes])
            leftoverChunks = [lastChunk[-leftoverBytes:]]
         else:
            leftoverChunks = []

         # Write chunks
         for chunk in chunks:
            self.wfile.write(chunk)
         self.wfile.write(b"\r\n")

         # Reset state
         self.currChunkSize = leftoverBytes
         self.chunks = leftoverChunks

      def write(self, buf):
         if not buf or not self.wfile:
            return

         if six.PY3 and isinstance(buf, six.string_types):
            buf = buf.encode()

         size = len(buf)
         self.currChunkSize += size

         self.chunks.append(buf)
         while self.currChunkSize >= self.chunkSize:
            self._WriteChunk()

      def close(self):
         if not self.wfile:
            return

         # Flush chunks
         self.flush()

         # Write ending zero bytes chunk
         self._WriteChunk()

         # TODO: Write trailer if needed

         # No more write
         self.wfile = None

      def __del__(self):
         self.close()

      def flush(self):
         # Flush buffer
         if self.currChunkSize:
            self._WriteChunk()

   ## Deflate (zlib) writer
   #
   class DeflateWriter:
      def __init__(self, wfile, compresslevel=5):
         import zlib
         self.wfile = wfile
         self.compress = zlib.compressobj(compresslevel,
                                          zlib.DEFLATED,
                                          zlib.MAX_WBITS)
         self.flushFlag = zlib.Z_SYNC_FLUSH
         self.closeFlushFlag = zlib.Z_FINISH

      def write(self, buf):
         if not buf or not self.wfile:
            return

         self.wfile.write(self.compress.compress(buf))

      def flush(self):
         if not self.wfile:
            return

         self.wfile.write(self.compress.flush(self.flushFlag))

      def close(self):
         if not self.wfile:
            return

         self.wfile.write(self.compress.flush(self.closeFlushFlag))
         self.wfile.close()

         # No more write
         self.wfile = None
         self.compress = None

      def __del__(self):
         self.close()

   # In all Python 3 versions up to 3.5, there is a bug that wfile.write
   # may write partial message. The bug is fixed in 3.6.
   # See https://bugs.python.org/review/26721/
   # This class works around the issue and make sure the complete message is
   # sent out.
   class _SafeWriter:
      def __init__(self, wfile):
         self.wfile = wfile

      def write(self, buf):
         if not buf or not self.wfile:
            return

         totalsent = 0
         buflen = len(buf)
         while totalsent < buflen:
            sent = self.wfile.write(buf[totalsent:])
            totalsent = totalsent + sent

   ## Find token from a comma separated tokens
   #
   def _FindToken(self, tokens, findToken):
      # Split token with ,
      for token in tokens.lower().split(","):
         # Split again with ;
         tokenAndVal = token.split(";", 1)
         if tokenAndVal[0].strip() == findToken:
            if len(tokenAndVal) > 1:
               val = tokenAndVal[1].strip()
            else:
               val = ""
            return (findToken, val)
      return None

   ## Handle HTTP Post
   #
   def do_POST(self):
      """ Handle HTTP Post """
      logging.debug("In do_POST: %s" % str(self.client_address))
      responseCode = 500
      response = ""
      closeConnection = False
      httpVersion = float(self.request_version.split("/")[1])

      # Handle SOAPAction (for request version in SOAPAction)
      # Can we have multiple SOAPAction??? No. I guess it is not valid
      soapAction = self.headers.get(_STR_SOAP_ACTION, "")
      version = GetVersion(soapAction)

      cookies = SimpleCookie(self.headers.get('cookie'))
      VmomiSupport.GetHttpContext()['cookies'] = cookies

      # Look for non-identity transfer-encoding before content-length
      reqChunking = False
      gzipResponse = False
      deflateResponse = False
      respChunking = False
      if httpVersion >= 1.1:
         # Request chunking
         xferEncoding = self.headers.get("transfer-encoding", "")
         reqChunking = (self._FindToken(xferEncoding, "chunked") is not None)

         # Accept-Encoding
         acceptEncoding = self.headers.get("accept-encoding", "")
         # Support gzip only for now
         gzipResponse = (self._FindToken(acceptEncoding, "gzip") is not None)
         if not gzipResponse:
            deflateResponse = (self._FindToken(acceptEncoding, "deflate") is not None)

         # Response chunking
         te = self.headers.get("TE", "chunked")
         respChunking = (self._FindToken(te, "chunked") is not None)

      if reqChunking:
         request = self._ChunkedMessageBodyReader(self.rfile)
      else:
         # Get content length. Max is 16 M
         maxContentLen = 16 * 1024 * 1024
         content_len = maxContentLen

         # Get content length from header
         contentLength = self.headers.get("content-length")
         if contentLength:
            try:
               contentLength = int(contentLength)
               if contentLength > maxContentLen:
                  # Larger than max content length allowed. Truncate length
                  logging.warn("Request content length %d > %d" % \
                                             (contentLength, maxContentLen))
                  contentLength = maxContentLen
               content_len = contentLength
            except TypeError:
               pass
         request = _MessageBodyReader(self.rfile, content_len)

      try:
         # Invoke handler
         responseCode, response = self.server.InvokeHandler(request, version)
         DebugLogFilteredXML(response)
      except Exception as err:
         assert(responseCode == 500)

      try:
         # Char encoding
         encoding = "utf-8"

         # Determine if response is string or not
         isStringResponse = isinstance(response, six.text_type) or \
                            isinstance(response, six.binary_type)
         if isinstance(response, six.text_type):
            response = response.encode(encoding)

         # Send Header
         self.send_response(responseCode)
         self.send_header("content-type", "text/xml; charset=" + encoding)
         self.send_header("cache-control", "no-cache")
         cookies = VmomiSupport.GetHttpContext()['cookies']
         for cookie in cookies:
            headerValue = cookies[cookie].output(header='')
            self.send_header("set-cookie", headerValue)

         hasContentLength = isStringResponse and not (gzipResponse or \
                                                      deflateResponse or \
                                                      respChunking)
         if hasContentLength:
            # Content length
            responseLen = len(response)
            self.send_header("content-length", str(responseLen))
         else:
            if not respChunking:
               closeConnection = True

         if httpVersion >= 1.1:
            # Chunking?
            if respChunking:
               self.send_header("transfer-encoding", "chunked")

            # Gzip content?
            if gzipResponse:
               self.send_header("content-encoding", "gzip")
            elif deflateResponse:
               self.send_header("content-encoding", "deflate")

            # Close connection?
            if closeConnection:
               self.send_header("connection", "close")
         else:
            if closeConnection:
               self.close_connection = 1

         # End headers
         self.end_headers()

         # Send response
         wfile = self.wfile
         needClose = False
         chunkSize = 8192

         # Work around the partial write bug in python 3 versions up to 3.5
         if sys.version_info.major == 3 and sys.version_info < (3, 6):
            wfile = self._SafeWriter(wfile=wfile)

         # Handle chunking
         if respChunking:
            wfile = self._ChunkedMessageBodyWriter(wfile, chunkSize)
            # Need explicit close for chunked response
            needClose = True

         # Handle compression
         if gzipResponse:
            import gzip
            wfile = gzip.GzipFile(fileobj=wfile, mode="wb")
            needClose = True
         elif deflateResponse:
            wfile = self.DeflateWriter(wfile=wfile)
            needClose = True

         if response:
            if isStringResponse:
               wfile.write(response)
            else:
               while True:
                  chunk = response.read(chunkSize)
                  if not chunk:
                     break
                  wfile.write(chunk)
            wfile.flush()

         if needClose:
            wfile.close()

         # Cleanup
         if wfile != self.wfile:
            del wfile

         if not isStringResponse:
            response.close()
         del response

         logging.debug("Done do_POST: %s" % str(self.client_address))
      except Exception as err:
         LogException("Error: Send response exception: ", err)
         self.close_connection = 1

   def log_message(self, format, *args):
      """
      Override the BaseHTTPServer.BaseHTTPRequestHandler method to send the
      log message to the log file instead of stderr.
      """
      logging.info("%s - - %s" % (self.client_address, format%args))


## Base handler for CGI requests
#
class CgiBaseHandler(object):
   """
   Base class for handling CGI requests.
   Attributes:
      * headers  - A dict containing CGI headers and their values
      * rfile    - a File-compatible object exposing a read method.  Used for
                   reading the request XML string.
      * wfile    - a File-compatible object exposing a write method.  Used for
                   writing the CGI response including headers.
   """
   def __init__(self, headers, rfile, wfile, stubs=None):
      self.headers = headers
      self.rfile = rfile
      self.wfile = wfile
      self.stubs = stubs

   @staticmethod
   def _CgiResponse(msg = '', statusmsg = '500 Internal Server Error',
                       contenttype = 'text/plain', extraheaders = ''):
      return 'Content-Type: %s\r\nStatus: %s\r\n%s\r\n%s\r\n' % (
         contenttype, statusmsg, extraheaders, msg
      )

   ##
   #  Handle one CGI request
   def HandleRequest(self):
      """protocol-independent CGI request handler.  Reads CONTENT_LENGTH bytes
         from rfile, processing the SOAP request using version and stubs.
         The REQUEST_METHOD must be POST.
         The response is written to wfile.
         If the REQUEST_METHOD is not POST, the content length is negative,
         or a fault happened, appropriate error responses are returned.
         Returns:
            A boolean, True if the response is a fault.
      """
      request_method = self.headers['REQUEST_METHOD']
      if request_method != 'POST':
         if 'REQUEST_URI' in self.headers:
            request_uri = self.headers['REQUEST_URI']
         else:
            request_uri = '%s/%s?%s' % (self.headers.get('SCRIPT_NAME', ''),
                                        self.headers.get('PATH_INFO', ''),
                                        self.headers.get('QUERY_STRING', ''))
         msg = ('HTTP method %s not supported for URL: %s' %
                (request_method, request_uri))
         logging.warning(msg)
         self.wfile.write(self._CgiResponse(msg,
            statusmsg='405 Method Not Allowed',
            extraheaders='Allow: POST\r\n'))
         self.wfile.flush()
         return True

      # Get soap request
      contentlen = int(self.headers['CONTENT_LENGTH'])
      if contentlen < 0:
         self.wfile.write(self._CgiResponse(statusmsg='400 Bad Request',
                                 msg='Content-Length is a negative value'))
         self.wfile.flush()
         return True
      soapRequest = _MessageBodyReader(self.rfile, contentlen)

      # Handle request
      version = GetVersion(self.headers.get('HTTP_SOAPACTION', ''))
      isFault, response = GetSoapHandler(self.stubs).HandleRequest(soapRequest,
                                                                   version)
      DebugLogFilteredXML(response)

      # Compose response string
      if isFault:
         status = '500 Internal Server Error'
      else:
         status = '200 OK'
      self.wfile.write(self._CgiResponse(statusmsg=status, msg=response,
                                         contenttype='text/xml').encode((UTF8)))
      self.wfile.flush()
      return isFault


## Thread pool for the http server
#
class HttpServerThreadPoolMixin(socketserver.ThreadingMixIn):
   """ Pooled SocketServer ThreadingMixin """

   @classmethod
   def SetMaxWorkers(cls, workers=8):
       """
        Set the threadpool size.
        0 - means infinite (i.e.) each client connection is handled in a new
        thread.
       """
       cls.thread_max_workers = workers
       # Because of persistent HTTP connections, there is no point
       # in having more # of connections queued than the # of available
       # workers. However, to maintain backward compatibility, max queued
       # limit is set to workers only when changing workers to larger than
       # max queue items.
       if workers > cls.thread_pool_max_queued_works:
          cls.thread_pool_max_queued_works = workers

   @classmethod
   def init(cls, maxWorkers=8, maxQueuedWorks=256):
      """ Init all class variables """
      cls.thread_quit_request = object()
      cls.thread_max_workers = maxWorkers
      cls.thread_pool_workers = 0
      cls.thread_pool_worker_list = None
      cls.thread_pool_workitems = None
      cls.thread_pool_max_queued_works = maxQueuedWorks

   def process_request_worker(self):
      """ Process request worker """
      while self.thread_looping:
         try:
            # Wait for request
            # TODO: Stop thread if it has been idle for a long time
            args = self.thread_pool_workitems.get()
            if args == self.thread_quit_request:
               # It's a request to exit the loop.
               break
            logging.debug("Handling workitem: " + str(args))

            # Start work
            socketserver.ThreadingMixIn.process_request_thread(self, *args)
            logging.debug("Done workitem: " + str(args))
         except Exception as err:
            LogException("Thread caught exception: ", err)
            time.sleep(1)
      logging.debug("%s:thread pool worker exiting" % threading.currentThread())

   def process_request(self, request, client_address):
      """
      Use a worker thread from pool to process this request
      Note: This is running in serialized context
      """

      # When max number of workers are unlimited then simply forward the
      # request to ThreadingMixIn (parent) class.
      if self.thread_max_workers == 0:
         socketserver.ThreadingMixIn.process_request(self, request, client_address)
         return

      if self.thread_pool_workitems == None:
         # First time init
         self.thread_pool_workitems = queue.Queue(0)
         self.thread_pool_worker_list = []
         self.thread_looping = True

      if self.thread_pool_workers < self.thread_max_workers:
         thd = threading.Thread(target=self.process_request_worker)
         thd.setDaemon(True)
         thd.start()
         self.thread_pool_worker_list.append(thd)
         self.thread_pool_workers += 1

      # Bound work items length
      workItem = (request, client_address)
      qSize = self.thread_pool_workitems.qsize()
      if qSize >= self.thread_pool_max_queued_works:
         # Drop request
         logging.error("Too many queued work (%d) Dropping workitem: %s" % \
                                                        (qSize, str(workItem)))
         self.close_request(request)

         # A crude way to prevent DoS: sleep
         time.sleep(1)
         return

      # Put work item into q to wake up worker thread
      self.thread_pool_workitems.put(workItem)
      logging.debug("Queued workitem: " + str(workItem))

   ## Shutdown the threads in the threadpool and wait for them to exit.  The
   #  BaseServer.shutdown() method should be called before calling this method.
   #  The worker threads are shutdown by sending them a "quit" request and then
   #  waiting for them to exit up until the given timeout.  No extra measures
   #  are taken to interrupt their current processing.  It is expected that the
   #  threads are relatively short lived or there is a low likelihood that
   #  something bad will happen if the process exits in the middle of
   #  processing.
   #
   #  @param timeout The number of seconds to wait for each worker thread.
   def shutdown_threadpool(self, timeout=1.0):
      # When max number of workers are unlimited there is no threadpool
      # to shutdown
      if self.thread_max_workers == 0:
        return
      logging.info("shutting down thread pool")
      self.thread_looping = False
      if self.thread_pool_worker_list:
          # Only go through the following operations if thread_pool_worker_list
          # is initialized.
          for _worker in self.thread_pool_worker_list:
             self.thread_pool_workitems.put(self.thread_quit_request)

          for worker in self.thread_pool_worker_list:
             worker.join(timeout)

## Soap http server class
#
class SoapHttpServer(HttpServerThreadPoolMixin, HTTPServer):
   """ Soap http server """

   @classmethod
   def SetMaxWorkers(cls, workers=8):
       logging.debug("Setting max workers: %s" % str(workers))
       HttpServerThreadPoolMixin.SetMaxWorkers(workers)

   # Init HttpServerThreadPoolMixin
   HttpServerThreadPoolMixin.init()

   # Soap stubs
   soapStubs = None

   # SSL parameters
   ssl = False
   sslArgs = {}
   ssl_wrap_socket = None

   ## Soap handler
   #
   # @param request the SOAP request
   # @param version the request version (namespace/versionId)
   # @return a tuple of (isFault, SOAP response)
   def InvokeHandler(self, request, version=None):
      """ Soap handler """
      isFault, response = GetSoapHandler(self.soapStubs).HandleRequest(request,
                                                                       version)
      return isFault and 500 or 200, response

   ## Set SSL arguments
   #
   # @param  keyfile Server PEM private key file
   # @param  certfile Server PEM certificate file
   # @param  kwargs Other ssl arguments
   def SetSSL(self, keyfile, certfile, **kwargs):
      try:
         import ssl
         self.ssl = True

         self.sslArgs = kwargs.copy()

         # Override ssl arguments
         self.sslArgs.setdefault("server_side", True)
         self.sslArgs.setdefault("keyfile", keyfile)
         self.sslArgs.setdefault("certfile", certfile)

         self.ssl_wrap_socket = ssl.wrap_socket
      except ImportError as err:
         logging.error("Failed to import ssl. Ssl not supported")

   ## Override get_request in BaseHTTPServer.HTTPServer
   #
   # @returns request and client address
   def get_request(self):
      newSock, fromAddr = HTTPServer.get_request(self)
      logging.debug("Connection from: %s" % str(fromAddr))

      # Disable nagle
      if newSock:
         try:
            import socket
            newSock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
         except Exception:
            pass

      if self.ssl:
         newSock = self.ssl_wrap_socket(newSock, **self.sslArgs)

      return newSock, fromAddr

   ## Shutdown the server and the worker threads.
   def shutdown(self):
      HTTPServer.shutdown(self)
      self.shutdown_threadpool()


## Http soap server over unix socket
#  Only available on unix like system
#
try:
   # Cleanup unix socket
   def CleanupUnixSocket(file):
      try:
         os.unlink(file)
      except Exception as err:
         pass

   from socketserver import UnixStreamServer
   class SoapHttpServerOverUnixSocket(UnixStreamServer, SoapHttpServer):
      pass
except ImportError as err:
   class SoapHttpServerOverUnixSocket:
      def __init__(self, *args, **kwargs):
         raise NotImplementedError("Unix stream server not available")


## SCGI over anonymous pipe server class
#
class ScgiServer(object):
   """
   SCGI server.  For more info, see http://python.ca/scgi/protocol.txt
   """
   def __init__(self, rfile=None, wfile=None, stubs=None):
      """Constructs an instance of ScgiServer.
         Parameters:
            * rfile - The input File object to read requests from.  If None,
                      defaults to sys.stdin.
            * wfile - The output File object to write responses to.  If None,
                      defaults to sys.stdout.
            * stubs - SOAP stub adapter object, used for remote invocation.
      """
      self.stubs = stubs
      self.rfile = rfile
      self.wfile = wfile
      self.debugout = None
      if rfile is None:
         self.rfile = getattr(sys.stdin, 'buffer', sys.stdin)
      if wfile is None:
         self.wfile = getattr(sys.stdout, 'buffer', sys.stdout)
         # Stop errant print statements from affecting output to stdout
         self.debugout = BytesIO()
         sys.stdout = self.debugout

      # NOTE: On Windows, need to set the input/output to binary mode to
      # prevent \n <--> \r\n translation.  The translation messes
      # up netstring output as the string length count becomes inaccurate.
      if sys.platform == "win32":
         import msvcrt
         msvcrt.setmode(self.wfile.fileno(), os.O_BINARY)
         msvcrt.setmode(self.rfile.fileno(), os.O_BINARY)

   ## Handle one request.  May be repeated in a loop.
   #
   def _HandleOneSCGIRequest(self):
      """Handles one SCGI request from filein.
         Returns:
            The response string to write out to the output stream, or
            '' if EOF has been reached on filein.
      """
      try:
         req = Scgi.ScgiRequest.Parse(self.rfile, readmessage=False)
         logging.debug('Got SCGI request with headers: %s' % (req.headers))
         responsefile = BytesIO()
         handler = CgiBaseHandler(req.headers, self.rfile, responsefile,
                                  self.stubs)
         handler.HandleRequest()
         if self.debugout:
            debugstr = self.debugout.getvalue()
            if debugstr:
               logging.debug(debugstr)
               self.debugout.close()
               self.debugout = BytesIO()
               sys.stdout = self.debugout
         return responsefile.getvalue()
      except Scgi.Scgi_EOF:
         return b''
      except KeyError as e:
         return CgiBaseHandler._CgiResponse('Missing header key: %s' % (e)).encode(UTF8)
      except Scgi.ScgiError as e:
         return CgiBaseHandler._CgiResponse('SCGI parsing error: %s' % (e)).encode(UTF8)
      except Exception as e:
         import traceback
         return CgiBaseHandler._CgiResponse('General exception: %s\n%s' %
                                            (e, traceback.format_exc())).encode(UTF8)

   def serve_forever(self):
      """Main server loop.
         Exits if the input pipe is closed.
      """
      while True:
         resp = self._HandleOneSCGIRequest()
         if not resp:
            logging.info('SCGI input pipe has been closed, exiting server')
            break
         Scgi.WriteNetString(self.wfile, resp)
         self.wfile.flush()


## Server main class
#
class ServerMain:
   """
   The python mo server main
   """

   ## Constructor
   def __init__(self):
      """
      Constructor
      """

      self.options = None
      self.remainingOptions = None
      self.httpd = None
      self.stdin = getattr(sys.stdin, 'buffer', sys.stdin)
      self.stdout = getattr(sys.stdout, 'buffer', sys.stdout)

   ## Start server
   #
   # @param argv the argument list
   def Start(self, options, remainingOptions):
      """
      Start the server
      """

      # Save internal options
      self.options = options
      self.remainingOptions = remainingOptions

      # Run server
      self._RunServer()

   ## Read xml helper. Avoid reading the whole request into memory
   #
   class _XmlFileReader:
      """ Read xml until eof """
      def __init__(self, rfile, buf):
         """ Constructor """
         self.rfile = rfile
         self.buf = buf

      def read(self, bytes):
         """ Read bytes """
         bufLen = len(self.buf)
         if bufLen > 0:
            if bufLen < bytes:
               ret = b"".join([self.buf, self.rfile.read(bytes - bufLen)])
               self.buf = b""
            else:
               ret = self.buf[:bytes]
               self.buf = self.buf[bytes:]
            return ret
         else:
            return self.rfile.read(bytes)

   ## Get soap request
   #
   # @param  fileIn Input file handle
   # @return (soapRequest, version) (Soap request, soap request version)
   def _GetSoapRequest(self, fileIn):
      """
      Get soap request from file handle
      """

      soapRequest = ""
      version = None
      while True:
         line = fileIn.readline()
         if not line:
            break
         line = line.strip()

         if line.startswith(b"<?xml"):
            soapRequest = self._XmlFileReader(fileIn, line)
            break
         elif line.startswith(_STR_SOAP_ACTION_BYTES + b":"):
            # Get SOAPAction
            urn = line[len(_STR_SOAP_ACTION_BYTES) + 1:].decode(UTF8)
            version = GetVersion(urn)

      return soapRequest, version

   ## Run server
   #
   def _RunServer(self):
      """
      Internal server start function
      Note that self.options can be passed in via an API and not necessarily via optparse
      So, please make sure to use getattr while accessing 'optional' fields
      """
      if getattr(self.options, 'rhost', None):
         from pyVmomi.SoapAdapter import SoapStubAdapter
         proxyStub = SoapStubAdapter(host=self.options.rhost,
                                     port=int(self.options.rport),
                                     ns=self.options.rns,
                                     path=self.options.rpath)
         stubs = {'default': (None, proxyStub)}
      else:
         stubs = None

      if getattr(self.options, 'interactive', False):
         # Handle soap request from stdin
         from six.moves import cStringIO

         # TODO: Verify logging is currently redirected to file
         #logging.disable(logging.CRITICAL + 1)

         # Turn off logging to stdout & stderr
         orgSysStdout = sys.stdout
         orgSysStderr = sys.stderr
         debugOut = cStringIO()

         # DEBUG: Remove the next 2 lines to show debug output
         sys.stdout = debugOut
         sys.stderr = debugOut

         # Get soap request
         soapRequest, version = self._GetSoapRequest(self.stdin)

         # Handle request
         isFault, response = GetSoapHandler(stubs).HandleRequest(soapRequest,
                                                                 version)

         # Write response after setting stdout back to original
         sys.stdout = orgSysStdout
         sys.stderr = orgSysStderr
         #print response.replace("\n","") # Strip linefeed
         print(response)
         sys.stdout = debugOut
         sys.stderr = debugOut

         # logging will crash if I close the debugOut with logging to stdout
         #debugOut.close()
         sys.exit(isFault and 1 or 0)
      elif getattr(self.options, 'file', None):
         # Handle soap request from file

         try:
            # Get soap request
            fileIn = open(self.options.file, "rb")
         except IOError:
            message = "Cannot open " + str(self.options.file)
            logging.error(message)
            print(message)
            return

         # Get soap request
         soapRequest, version = self._GetSoapRequest(fileIn)

         # Handle request
         isFault, response = GetSoapHandler(stubs).HandleRequest(soapRequest,
                                                                 version)
         fileIn.close()
         print(response)
         sys.exit(isFault and 1 or 0)
      elif getattr(self.options, 'cgi', False):
         logging.info("Starting CGI server on stdin/stdout")
         handler = CgiBaseHandler(os.environ, self.stdin, self.stdout, stubs)
         try:
            isFault = handler.HandleRequest()
         except KeyError as err:
            isFault = True
            print((handler._CgiResponse("Missing environment variable " + str(err))))
         sys.exit(isFault and 1 or 0)
      elif getattr(self.options, 'scgi', False):
         import Scgi
         global Scgi
         logging.info("Starting SCGI server on stdin/stdout")
         scgid = ScgiServer(stubs=stubs)
         scgid.serve_forever()
         sys.exit(0)
      else:
         # Handle soap request from http
         if getattr(self.options, 'unix', None):
            addr = self.options.unix
            soapHttpdConstructor = SoapHttpServerOverUnixSocket
         elif getattr(self.options, 'port', None):
            # "" is the same as local host
            h = getattr(self.options, 'host', "")
            addr = (h, int(self.options.port))
            soapHttpdConstructor = SoapHttpServer
         else:
            print("Must specific a socket address")
            return

         logging.info("Listening on %s" % str(addr))
         soapHttpd = soapHttpdConstructor(addr, GeneralHttpHandler)
         self.httpd = soapHttpd
         if soapHttpdConstructor == SoapHttpServerOverUnixSocket:
            # Make sure we cleanup the unix socket file
            import atexit
            atexit.register(CleanupUnixSocket, addr)

         # set the custom worker thread pool size
         if soapHttpdConstructor == SoapHttpServer:
            soapHttpd.SetMaxWorkers(self.options.max_workers)

         keyfile = getattr(self.options, 'keyfile', None)
         certfile = getattr(self.options, 'certfile', None)
         if keyfile and certfile:
            soapHttpd.SetSSL(keyfile, certfile)
         soapHttpd.soapStubs = stubs
         soapHttpd.serve_forever()


## Main
#
def Run():
   """
   Server main
   """

   try:
      ServerMain().Start(gCmdOptions, gCmdRemainingOptions)
   except KeyboardInterrupt:
      print('^C received, shutting down server')
   except Exception as err:
      message = str(err)
      if message:
         print(message)
         logging.info(message)


if __name__ == "__main__":
   Run()
