#!/usr/bin/python
# -*- coding: iso-8859-1 -*-
# Copyright (c) 2005 Patrik Simons, Neotide Ab
# License: MIT
"""
A Quixote handler for Python ISAPI (in pywin32).
Needs a threadsafe publisher.

The Quixote demo:
# Use as a script to install. Pass remove as a command line
# argument to uninstall. Keep this script where you want the DLL to be stored.
# Remember that the "Launch IIS Process Account" (IWAM_something) account needs
# access rights to the DLL.
from isapi_handler import setup, QuixoteExtension

def create_publisher():
    from quixote.publish import Publisher
    from quixote.demo.mini_demo import RootDirectory
    return Publisher(RootDirectory(), display_exceptions='plain')

name = 'quixote'

# The entry point for the ISAPI extension.
def __ExtensionFactory__():
    return QuixoteExtension(name)

setup(name, 'Quixote demo', create_publisher)
"""
import platform, sys, traceback, urllib
from cStringIO import StringIO
from isapi import isapicon, ExtensionError
from isapi.threaded_extension import ThreadPoolExtension
from quixote.publish import get_publisher
from quixote.http_request import HTTPRequest

class EcbDict(dict):
    def __init__(self, ecb):
        self.ecb = ecb

    def __getitem__(self, key):
        try:
            return dict.__getitem__(self, key)
        except KeyError:
            item = self.ecb.GetServerVariable(key, None)
            if item is None:
                raise
            self[key] = item
            return item

    def __contains__(self, key):
        if dict.__contains__(self, key):
            return True
        item = self.ecb.GetServerVariable(key, None)
        if item is None:
            return False
        self[key] = item
        return True

    def has_key(self, key):
        return self.__contains__(key)

    def get(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default

class QuixoteExtension(ThreadPoolExtension):
    """Quixote PyISAPI extension"""
    def __init__(self, script_name):
        self.script_name = script_name
        ThreadPoolExtension.__init__(self)

    def Dispatch(self, ecb):
        env = EcbDict(ecb)
        env['SCRIPT_NAME'] = '/'+self.script_name # It's wrong in the ecb
        env['REQUEST_URI'] = env.get('URL', '') # Apache style
        # Cache these, so we get them if env is copied.
        env.get('HTTP_HOST') # Used by HttpRequest.get_server().
        env.get('REMOTE_ADDR') # Used by session.user.log_user().
        # Fix PATH_INFO, IIS first decodes it and then encodes it into latin-1 or something.
        url = env.get('UNENCODED_URL')
        if url:
            q = url.find('?')
            if q > -1:
                url = url[:q]
            env['PATH_INFO'] = urllib.unquote(url)
        if env.get('SERVER_PROTOCOL') == 'HTTP/1.0':
            keep_alive = False # HTTP/1.0 doesn't understand transfer-codings
        else:
            keep_alive = env.get('HTTP_CONNECTION', '').lower() != 'close'
        try:
            publisher = get_publisher()
            if ecb.AvailableBytes > 0:
                data = ecb.AvailableData
                if ecb.TotalBytes > ecb.AvailableBytes:
                    data += ecb.ReadClient(ecb.TotalBytes - ecb.AvailableBytes)
            else:
                data = ''
            request = HTTPRequest(StringIO(data), env)
            response = publisher.process_request(request)
            length = response.get_content_length()
            headers = ['%s: %s' % (n,v) for (n,v) in response.generate_headers()]
            if keep_alive and length <= 1: # Greater than one because IIS treats space as nothing
                headers.append('Transfer-Encoding: chunked')
            headers.append('\r\n')
            status = '%s %s' % (response.status_code, response.reason_phrase)
            # If keep-alive is enabled, IIS stops responding if many requests from the same session
            # comes in quick succession. The problem is that for empty responses IIS does not add a
            # Content-Length header, so the browser doesn't know when the request ends. Therefore,
            # it doesn't reuse the connection before a timeout expires and this is experienced as a
            # hang. We can't add the header ourselves as IIS strips it out. The solution is to use
            # the header "Transfer-Encoding: chunked" and send the response body in length prefixed
            # chunks.
            ecb.SendResponseHeaders(status, '\r\n'.join(headers), keep_alive)
            if keep_alive and (not length or length <= 1):
                if length:
                    ecb.WriteClient('%x\r\n' % length)
                    for chunk in response.generate_body_chunks():
                        ecb.WriteClient(chunk+'\r\n')
                ecb.WriteClient('0\r\n\r\n') # Last chunk is empty
            else:
                for chunk in response.generate_body_chunks():
                    ecb.WriteClient(chunk)
            ecb.DoneWithSession(isapicon.HSE_STATUS_SUCCESS_AND_KEEP_CONN and keep_alive \
                                or isapicon.HSE_STATUS_SUCCESS)
        except ExtensionError:
            ecb.DoneWithSession() # Client disconnected.
        except:
            publisher.log(''.join(traceback.format_exception(*sys.exc_info())))
            ecb.DoneWithSession()

def create_pool(name, username=None, password=None, app_pool_identity_type=None):
    from isapi.install import GetObject, _IIS_OBJECT
    import pywintypes
    pool_name = name + '_pool'
    try:
        GetObject(_IIS_OBJECT + '/AppPools/' + pool_name)
    except pywintypes.com_error:
        # The pool does not exist
        pools = GetObject(_IIS_OBJECT + '/AppPools')
        pool = pools.Create('IIsApplicationPool', pool_name)
        if username:
            pool.WAMUserName = username
            pool.WAMUserPass = password
            pool.AppPoolIdentityType = 3 # Specified user account
        elif app_pool_identity_type is not None:
            # 0: LocalSystem
            # 1: LocalService
            # 2: NetworkService
            # 3: ApplicationPoolIdentity
            pool.AppPoolIdentityType = app_pool_identity_type

        # Prevent IIS from restarting the process as we sometimes have long jobs
        pool.PeriodicRestartTime = 0
        pool.IdleTimeout = 0
        if platform.architecture()[0] == '32bit':
            try:
                pool.Enable32BitAppOnWin64 = True
            except AttributeError:
                pass
        pool.SetInfo()
    return pool_name

def delete_pool(name):
    from isapi.install import GetObject, _IIS_OBJECT
    import pywintypes
    pool_name = name.replace('/', '_') + '_pool'
    pools = GetObject(_IIS_OBJECT + '/AppPools')
    try:
        pools.Delete('IIsApplicationPool', pool_name)
    except pywintypes.com_error:
        pass # The pool didn't exist

def install(name, description, argv=None, auth_anonymous=False, username=None, password=None, app_pool_identity_type=None, only_ntlm_authentication=None):
    """Installs the DLL and sets up the virtual directory in IIS.

    If name is a path, then you will have to create the parent directories
    in IIS yourself, using, for instance, the Internet Service Manager GUI."""
    from isapi.install import ISAPIParameters, ScriptMapParams, \
         VirtualDirParameters, HandleCommandLine, _APP_OUTPROC, _APP_INPROC
    release, version, csd, ptype = platform.win32_ver()
    if release == '2000':
        # Windows 2000
        AddExtensionFile = False
        if csd <= 'SP2':
            AppProtection = _APP_INPROC # Doesn't work with more than one extension.
        else: # Service pack 3 is untested
            AppProtection = _APP_OUTPROC # Works with many extensions.
    else:
        AddExtensionFile = True
        AppProtection = _APP_OUTPROC # Works with many extensions.
    def set_authentication_and_app_pool(params, options, webdir=None):
        if webdir:
            if version >= '5.2':
                # Windows 2003 or later, _APP_OUTPROC does not work, we need our own AppPool.
                pool_name = create_pool(name.replace('/', '_'), username, password, app_pool_identity_type)
                webdir.AppPoolID = pool_name
            if auth_anonymous:
                webdir.AuthNTLM = False
                webdir.AuthAnonymous = True
            else:
                webdir.AuthNTLM = True
                webdir.AuthAnonymous = False
                if only_ntlm_authentication:
                    webdir.NTAuthenticationProviders = 'NTLM' # Kerberos authentication doesn't always work, see http://support.microsoft.com/kb/871179
            webdir.AspAllowSessionState = False
            webdir.SetInfo()
    def remove_pool(params, options):
        if version >= '5.2':
            delete_pool(name)
    params = ISAPIParameters()
    sm = [ScriptMapParams(Extension='*', Flags=0, AddExtensionFile=AddExtensionFile)]
    if '/' in name:
        path = name.rsplit('/', 1)[0]
    else:
        path = None
    vd = VirtualDirParameters(Name=name,
                              Description=description,
                              AppProtection=AppProtection,
                              Path=path,
                              ScriptMaps=sm,
                              ScriptMapUpdate='replace',
                              PostInstall=set_authentication_and_app_pool,
                              PostRemove=remove_pool)
    params.VirtualDirs = [vd]
    HandleCommandLine(params, argv=argv)

def setup(name, description, create_publisher, argv=None, auth_anonymous=False):
    if hasattr(sys, 'isapidllhandle'):
        # Catch early errors with PythonWin and
        # the Tools/Trace Collector Debugging tool.
        import win32traceutil
        # Imported by IIS. Create the publisher.
        create_publisher()
    else:
        # Install. Set argv=['remove'] or give remove
        # as a command line argument to uninstall.
        install(name, description, argv, auth_anonymous)
