# -*- coding: iso-8859-15 -*-


# Glasnost
# By: Odile Bénassy <obenassy@entrouvert.com>
#     Romain Chantereau <rchantereau@entrouvert.com>
#     Nicolas Clapiès <nclapies@easter-eggs.org>
#     Pierre-Antoine Dejace <padejace@entrouvert.be>
#     Thierry Dulieu <tdulieu@easter-eggs.com>
#     Florent Monnier <monnier@codelutin.com>
#     Cédric Musso <cmusso@easter-eggs.org>
#     Frédéric Péters <fpeters@entrouvert.be>
#     Benjamin Poussin <poussin@codelutin.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#     Sébastien Régnier <regnier@codelutin.com>
#     Emmanuel Saracco <esaracco@easter-eggs.com>
#
# Copyright (C) 2000, 2001 Easter-eggs & Emmanuel Raviart
# Copyright (C) 2002 Odile Bénassy, Code Lutin, Thierry Dulieu, Easter-eggs,
#     Entr'ouvert, Frédéric Péters, Benjamin Poussin, Emmanuel Raviart,
#     Emmanuel Saracco & Théridion
# Copyright (C) 2003 Odile Bénassy, Romain Chantereau, Nicolas Clapiès,
#     Code Lutin, Pierre-Antoine Dejace, Thierry Dulieu, Easter-eggs,
#     Entr'ouvert, Florent Monnier, Cédric Musso, Ouvaton, Frédéric Péters,
#     Benjamin Poussin, Rodolphe Quiédeville, Emmanuel Raviart, Sébastien
#     Régnier, Emmanuel Saracco, Théridion & Vecam
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


# This file:
#
# Contains works derived from xmlrpcServer.py (an xmlrpc server framework for
# python), Copyright (C) 2001 by Ken McIvor.
# xmlrpcServer.py is availabe at http://mrken.net/code/xml-rpc/xmlrpcServer.py
#
# Contains works derived from xmlrpcserver.py (a simple XML-RPC server for
# Python), Copyright (C) 1999 by Fredrik Lundh, Copyright (C) 1999 by Secret 
# Labs AB.  xmlrpcserver.py is available as part of version 0.9.8 of 
# PythonWare's XML-RPC library, found at
# http://www.pythonware.com/downloads/index.htm
#
# Contains works derived from xmlrpc_registry.py (A method registry for use with
# xmlrpclib), Copyright (C) 2001 by Eric Kidd (all rights reserved).
# xmlrpc_registry.py is available at
# http://xmlrpc-c.sourceforge.net/hacks/xmlrpc_registry.py
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission. 
#  
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

# This files also contains code derived from SecureXMLRPCServer.py by Michal
# Wallace and distributed with pyopenssl (and placed in the public domain).

__version__ = '$Revision$'[11:-2]


import BaseHTTPServer
import cStringIO
import marshal
import os
import socket
import SocketServer
import sys
import threading
import time
import traceback
import xmlrpclib

try:
    from OpenSSL import SSL
except ImportError:
    SSL = None

from glasnost.common.cache import temporaryCache
import glasnost.common.context as context
import glasnost.common.faults as faults

from glasnost.proxy.DispatcherProxy import callServer
from glasnost.proxy.tools import getProxyForServerRole


# Some type names for use in method signatures.
INT = 'int'
BOOLEAN = 'boolean'
DOUBLE = 'double'
STRING = 'string'
DATETIME = 'dateTime.iso8601'
BASE64 = 'base64'
ARRAY = 'array'
STRUCT = 'struct'


# Some error codes, borrowed from xmlrpc-c.
INTERNAL_ERROR = -500
TYPE_ERROR = -501
INDEX_ERROR = -502
PARSE_ERROR = -503
NETWORK_ERROR = -504
TIMEOUT_ERROR = -505
NO_SUCH_METHOD_ERROR = -506
REQUEST_REFUSED_ERROR = -507
INTROSPECTION_DISABLED_ERROR = -508
LIMIT_EXCEEDED_ERROR = -509
INVALID_UTF8_ERROR = -510


class NotificationThread(threading.Thread):
    def __init__(self, subscriberId, callBack, params):
        threading.Thread.__init__(self)
        self.subscriberId = subscriberId
        self.callBack = callBack
        self.params = params

    def run(self):
        try:
            callServer(self.subscriberId, self.callBack, self.params)
        except:
            pass # notification failed. Who should we warn?


#
# Servers.
#


class ServerMixin: 
    __server_version = 'RpcServer/' + __version__
    __python_version = 'Python ' + sys.version.split()[0]
    _allowIntrospection = 1
    monthName = [None,
                 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

    def __init__ (self):
        self._methods = {}
        self._wrappedMethods = {}
        self._wrapper = None
        self._signatures = {}
        self._help = {}
        self._capabilities = {}
        self._default_method = None
        self._subscriptions_before = {}
        self._subscriptions_after = {}
        self.log_message('%s starting up', self.version_string())
        if self._allowIntrospection:
            state = 'enabled'
        else:
            state = 'disabled'
        self.log_message('Introspection is %s', state)
        self._install_system_methods()

    def _install_system_methods(self):
        self.add_method('system.listMethods',
                        self.system_listMethods,
                        [[ARRAY]])
        self.add_method('system.methodSignature',
                        self.system_methodSignature,
                        [[ARRAY, STRING]])
        self.add_method('system.methodHelp',
                        self.system_methodHelp,
                        [[STRING, STRING]])
        self.add_method('system.multicall',
                        self.system_multicall,
                        [[ARRAY, ARRAY]])
        self.add_method('system.getCapabilities',
                        self.system_getCapabilities,
                        [[STRUCT]])

    def _introspection_check(self):
        if not self._allowIntrospection:
            raise xmlrpclib.Fault(INTROSPECTION_DISABLED_ERROR,
                ("Introspection has been disabled on this server."))

    def _no_such_method(self, name):
        raise xmlrpclib.Fault(NO_SUCH_METHOD_ERROR, str(name))

    def add_capability(self, name, specUrl, specVersion):
        self._capabilities[name] = {
                'specUrl': str(specUrl),
                'specVersion': int(specVersion),
                }
        self.log_message('add_capability %s %s %s', name, specUrl, 
                         specVersion)

    def add_method(self, name, method, signature = None, help = None):
        """Add a method to the Xml Rpc methods dictionnary.
        
        Keyword argument:
        =================

        *name*:
            The public method name string.
            
        *method*:
            The internal Xml Rpc handler method name string.
            
        *signature*:
            + A signature is an array of types. The first of these types is the
              return type of the method, the rest are parameters.
            + Because multiple signatures (ie. overloading) is permitted, a
              list of signatures rather than a singleton is prefered.
            + Default None.
            
        *help*:
            + The documentation string describing the use of that method. 
            + The documentation string may contain HTML markup. 
            + Default None.
            
        """
        
        if help is None:
            help = ''
        if signature is None:
            signature = 'undef'

        self._methods[name] = method
        self._signatures[name] = signature
        self._help[name] = help
        self.log_message('add_method %s', name)

    def add_subscription(self, when, virtualServerId, functionName,
                         subscriberId, callBackName):
        if when == 'after':
            subscriptions = self._subscriptions_after
        else:
            subscriptions = self._subscriptions_before
            
        listSubscriptions = subscriptions.setdefault(virtualServerId, [])
        listSubscriptions.append( (functionName, subscriberId, callBackName) )

    def add_wrappedMethod(self, name, method, signature = None, help = None):
        """Add a wrapped method to the Xml Rpc methods dictionnary.

        This is a slight modification of the add_method method.
        """
        
        if help is None:
            help = ''
        if signature is None:
            signature = 'undef'

        self._wrappedMethods[name] = method
        self._signatures[name] = signature
        self._help[name] = help
        self.log_message('add_wrappedMethod %s', name)

    def dispatch_call(self, name, params, isDirectCall = 0):
        useWrapper = 0
        if self._methods.has_key(name):
            method = self._methods[name]
        elif self._wrappedMethods.has_key(name):
            method = self._wrappedMethods[name]
            useWrapper = 1
        else:
            method = self._default_method
        if method == None:
            self._no_such_method(name)
        try:
            if params:
                self.send_before_notify(params[0], name, params)
            if not isDirectCall:
                context.initFromOther(self.baseContext)
                currentContext = context.get()
                temporaryCache.checkCachedValues()
                currentContext.setVar('cache', temporaryCache)
                currentContext.setVar('temporaryCache', temporaryCache)
            if useWrapper and self._wrapper is not None:
                result = self._wrapper(method, params, isDirectCall)
            else:
                result = apply(method, params)
            if not isDirectCall:
                assert currentContext == context.get()
            if params:
                self.send_after_notify(params[0], name, result)
            return result
        except faults.Fault:
            self.log_error(
                'error for request %s(%s) on %s', name, params, method)
            traceback.print_exc()
            raise
        except:
            self.log_error(
                'error for request %s(%s) on %s', name, params, method)
            f = cStringIO.StringIO()
            traceback.print_exc(f)
            f = f.getvalue()
            print f
            # TODO(?): send talkback with f
            raise faults.UnknownServerException()

    def log_date_time_string(self):
        now = time.time()
        year, month, day, hh, mm, ss, x, y, z = time.localtime(now)
        s = "%02d/%3s/%04d %02d:%02d:%02d" % (
                day, self.monthName[month], year, hh, mm, ss)
        return s

    def log_error(self, format, *args):
        self.write_error("%s [%s] %s" % (self._address,
            self.log_date_time_string(), format % args))

    def log_exception(self, method, info):
        tb = traceback.extract_tb(info[2])
        self.log_error(
                'Exception %s "%s" %s %s %s',
                method,
                traceback.format_exception_only(info[0], info[1])[0].strip(),
                tb[-1][0], tb[-1][1], tb[-1][2])

    def log_fault(self, method, fault):
        self.log_error('Fault %s %s "%s"', method, fault.faultCode,
                       fault.faultString)

    def log_message(self, format, *args):
        self.write_message("%s [%s] %s" % (self._address, \
            self.log_date_time_string(), format % args))

    def remove_subscription(self, when, virtualServerId, functionName,
                            subscriberId, callBackName):
        if when == 'any':
            self.remove_subscription(
                'before', virtualServerId, functionName, subscriberId,
                callBackName)
            self.remove_subscription(
                'after', virtualServerId, functionName, subscriberId,
                callBackName)
            return

        if when == 'after':
            subscriptions = self._subscriptions_after
        else:
            subscriptions = self._subscriptions_before
        listSubscriptions = subscriptions.setdefault(virtualServerId, [])
        subscriptions[virtualServerId] = [
            x
            for x in listSubscriptions
            if x[1] != subscriberId or (
                (x[0] != functionName and functionName != '_ALL_') \
                and (x[2] != callBackName and callBackName != 'any'))]

    def send_after_notify(self, virtualServerId, functionName, result):
        if functionName.startswith('system.') or type(virtualServerId) != '':
            return
        subscriptions = self._subscriptions_after
        for name, subscriber, callBack in subscriptions.setdefault(
            virtualServerId, []):
            if name not in (functionName, '_ALL_'):
                continue
            self.send_notify(
                subscriber, callBack, [virtualServerId, functionName, result])
    
    def send_before_notify(self, virtualServerId, functionName, params):
        if functionName.startswith('system.') or type(virtualServerId) != '':
            return
        subscriptions = self._subscriptions_before
        
        for name, subscriber, callBack in subscriptions.setdefault(
            virtualServerId, []):
            if name not in (functionName, '_ALL_'):
                continue
            self.send_notify(
                subscriber, callBack, [virtualServerId, functionName, params])

    def send_notify(self, subscriberId, callBack, params):
        notifThread = NotificationThread(subscriberId, callBack, params)
        notifThread.start()
        
    def set_default_method(self, method):
        self._default_method = method
        
    def set_wrapper(self, method):
        self._wrapper = method
    def system_getCapabilities(self):
        self._introspection_check()
        return self._capabilities
        
    def system_listMethods(self):
        self._introspection_check()
        return self._methods.keys() + self._wrappedMethods.keys()

    def system_methodHelp(self, name):
        self._introspection_check()
        if self._help.has_key(name):
            return self._help[name]
        else:
            self._no_such_method(name)

    def system_methodSignature(self, name):
        self._introspection_check()
        if self._signatures.has_key(name):
            return self._signatures[name]
        else:
            self._no_such_method(name)

    def system_multicall(self, calls):
        results = []
        for call in calls:
            # XXX: individual faults and exceptions are not logged
            # probable solution: move exception handling from do_POST to
            # dispatch_call
            try:
                name = call['methodName']
                params = call['params']
                if name == 'system.multicall':
                    errmsg = "Recursive system.multicall forbidden"
                    raise xmlrpclib.Fault(REQUEST_REFUSED_ERROR, errmsg)
                print '  multicall, calling', name
                result = [self.dispatch_call(name, params, isDirectCall = 1)]
            except xmlrpclib.Fault, fault:
                result = {'faultCode': fault.faultCode,
                          'faultString': fault.faultString}
            except:
                info = sys.exc_info()
                errmsg = "%s:%s" % (info[0], info[1])
                result = {'faultCode': 1, 'faultString': errmsg}
            results.append(result)
        return results

    def version_string(self):
        return self.__server_version + ' (' + self.__python_version + ')'

    def write_error(self, message):
        sys.stderr.write("ERROR: %s\n" % message)

    def write_message(self, message):
        sys.stderr.write("LOG: %s\n" % message)
    

class FastServer(ServerMixin, SocketServer.UnixStreamServer):
    __server_version = 'FastServer/' + __version__

    def __init__(self, serverAddress):
        socketPath = '/tmp/.glasnost:%s' % serverAddress[1]
        try:
            os.unlink(socketPath)
        except OSError:
            pass
        SocketServer.UnixStreamServer.__init__(
                self, socketPath, FastRequestHandler)
        os.chmod(socketPath, 0777)

        self._address = socketPath

        ServerMixin.__init__(self)

    def serve_forever(self):
        self.log_message('Awaiting Fast connections')
        SocketServer.UnixStreamServer.serve_forever(self)
        self.log_message('Finished serving Fast connections')


class FastForkingServer(SocketServer.ForkingMixIn, FastServer):
    pass


class FastThreadingServer(SocketServer.ThreadingMixIn, FastServer):
    pass


class XmlRpcServer(ServerMixin, SocketServer.TCPServer):
    __server_version = 'XmlRpcServer/' + __version__
    allow_reuse_address = 1

    def __init__(self, serverAddress):
        SocketServer.TCPServer.__init__(self, serverAddress,
                XmlRpcRequestHandler)

        if serverAddress[0] == '':
            host = socket.gethostbyaddr(socket.gethostname())[0]
        else:
            host = serverAddress[0]
        self._address = '%s:%s' % (host, serverAddress[1])

        ServerMixin.__init__(self)

    def serve_forever(self):
        self.log_message('Awaiting XML-RPC connections')
        SocketServer.TCPServer.serve_forever(self)
        self.log_message('Finished serving XML-RPC connections')


class XmlRpcThreadingServer(SocketServer.ThreadingMixIn, XmlRpcServer):
    pass


class XmlRpcForkingServer(SocketServer.ForkingMixIn, XmlRpcServer):
    pass
    


if SSL:
    class SSLBugFix:
        """
        SSL Connection tends to die on sendall,
        so I use send() as a workaround. This is
        called by socket._fileobject, which is needed
        so SocketServer (and kids) can treat the connection
        as a regular file.
        """
        def __init__(self, conn):
            """
            For some reason, I can't subclass Connection,
            so I'm making a proxy, instead.
            """
            self.__dict__["conn"] = conn
        def __getattr__(self,name):
            return getattr(self.__dict__["conn"], name)
        def __setattr__(self,name, value):
            setattr(self.__dict__["conn"], name, value)

        
        def sendall(self, data):
            """
            This is the bugfix. Connection.sendall() segfaults
            on socket._fileobject.flush(), so just rewire it
            to use send() instead.
            """
            self.__dict__["conn"].send(data)

        def shutdown(self, how=1):
            """
            This isn't part of the bugfix. SimpleXMLRpcServer.doPOST
            calls shutdown(1), and Connection.shutdown() doesn't take
            an argument. So we just discard it:
            """
            self.__dict__["conn"].shutdown()

        def accept(self):
            """
            This is the other part of the shutdown() workaround.
            Since servers create new sockets, we have to infect
            them with our magic. :)
            """
            c, a = self.__dict__["conn"].accept()
            return (SSLBugFix(c), a)



    class SecureTCPServer(SocketServer.TCPServer):
        """
        Just like TCPServer, but use a socket.
        This really ought to let you specify the key and certificate files.
        """
        def __init__(self, server_address, sslContext, RequestHandlerClass):
            SocketServer.BaseServer.__init__(self, server_address, RequestHandlerClass)

            self.socket = SSLBugFix(SSL.Connection(sslContext,
                    socket.socket(self.address_family, self.socket_type)))
            self.server_bind()
            self.server_activate()

    class SecureXmlRpcServer(ServerMixin, SecureTCPServer):
        __server_version = 'SecureXmlRpcServer/' + __version__

        def __init__(self, serverAddress, sslContext):
            SecureTCPServer.__init__(self, serverAddress, sslContext,
                    SecureXmlRpcRequestHandler)

            if serverAddress[0] == '':
                host = socket.gethostbyaddr(socket.gethostname())[0]
            else:
                host = serverAddress[0]
            self._address = '%s:%s' % (host, serverAddress[1])

            ServerMixin.__init__(self)

        def serve_forever(self):
            self.log_message('Awaiting XML-RPC/SSL connections')
            SecureTCPServer.serve_forever(self)
            self.log_message('Finished serving XML-RPC/SSL connections')


    class SecureXmlRpcThreadingServer(SocketServer.ThreadingMixIn, SecureXmlRpcServer):
        pass


    class SecureXmlRpcForkingServer(SocketServer.ForkingMixIn, SecureXmlRpcServer):
        pass


#
# Request Handlers.
#


class FastRequestHandler(SocketServer.StreamRequestHandler):
    def __init__(self, request, client_address, server):
        self.server = server
        SocketServer.StreamRequestHandler.__init__(
                self, request, client_address, server)
        
    def handle(self):
        bytes = self.rfile.readline()
        data = marshal.loads(self.rfile.read(int(bytes)))
        params, method = data['params'], data['methodName']
        self.server.log_message('Begin of request: %s', method)
        try:
            response = self.server.dispatch_call(method, params)
            if response is None:
                response = 0
            if type(response) != type(()):
                response = (response,)
        except xmlrpclib.Fault, e:
            response = [e.faultCode, e.faultString]
        except:
            info = sys.exc_info()
            e = xmlrpclib.Fault(
                    1,
                    "%s" % traceback.format_exception_only(
                            info[0], info[1])[0].strip())
            response = [e.faultCode, e.faultString]
        response = marshal.dumps(response)
        self.wfile.write('%11d\n' % len(response))
        self.wfile.write(response)
        self.wfile.flush()
        #self.wfile.close()
        self.server.log_message('End of request: %s', method)
    

class XmlRpcRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    def __init__(self, request, client_address, server):
        self.server = server
        BaseHTTPServer.BaseHTTPRequestHandler.__init__(
                self, request, client_address, server)

    def do_POST(self):
        data = self.rfile.read(int(self.headers["content-length"]))
        params, method = xmlrpclib.loads(data)
        self.server.log_message('Begin of request: %s', method)
        try:
            response = self.server.dispatch_call(method, params)
            if response is None:
                response = 0
            if type(response) != type(()):
                response = (response,)
        except xmlrpclib.Fault, fault:
            self.server.log_fault(method, fault)
            response = xmlrpclib.dumps(fault)
        except:
            info = sys.exc_info()
            self.server.log_exception(method, info)
            response = xmlrpclib.dumps(xmlrpclib.Fault(1, "%s" %
                traceback.format_exception_only(info[0], info[1])[0].strip()))
        else:
            try:
                response = xmlrpclib.dumps( response, methodresponse=1)
            except: # TODO: tighter check
                info = sys.exc_info()
                self.server.log_exception(method, info)
                response = xmlrpclib.dumps(xmlrpclib.Fault(
                        1,
                        "%s" % traceback.format_exception_only(
                                info[0], info[1])[0].strip()))
            self.server.log_message('End of request: %s', method)

        self.send_response(200)
        self.send_header("Content-type", "text/xml")
        self.send_header("Content-length", str(len(response)))
        self.end_headers()
        self.wfile.write(response)
        self.wfile.flush()
        self.connection.shutdown(1)


class SecureXmlRpcRequestHandler(XmlRpcRequestHandler):
    def setup(self):
        """
        We need to use socket._fileobject Because SSL.Connection
        doesn't have a 'dup'. Not exactly sure WHY this is, but
        this is backed up by comments in socket.py and SSL/connection.c
        """
        self.connection = self.request # for doPOST
        self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
        self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)

