# mod_python script

# ipaxmlrpc - an XMLRPC interface for ipa.
# Copyright (c) 2007 Red Hat
#
#    IPA is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; 
#    version 2.1 of the License.
#
#    This software 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this software; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
#
# Based on kojixmlrpc - an XMLRPC interface for koji by
# Mike McLean <mikem@redhat.com>
#
# Authors:
# Rob Crittenden <rcritten@redhat.com>

import sys


import time
import traceback
import pprint
from xmlrpclib import Marshaller,loads,dumps,Fault
from mod_python import apache
import logging

from ipaserver import funcs
from ipa import ipaerror, ipautil
import ldap

import string
import base64

#
# An override so we can base64 encode all outgoing values. 
# This is set by calling: Marshaller._Marshaller__dump = xmlrpclib_dump
#
# Not currently used.
#
def xmlrpclib_escape(s, replace = string.replace):
    """
    xmlrpclib only handles certain characters. Lets encode the whole
    blob
    """

    return base64.encodestring(s)

def xmlrpclib_dump(self, value, write):
    """
    xmlrpclib cannot marshal instances of subclasses of built-in
    types. This function overrides xmlrpclib.Marshaller.__dump so that
    any value that is an instance of one of its acceptable types is
    marshalled as that type.

    xmlrpclib also cannot handle invalid 7-bit control characters. See
    above.
    """

    # Use our escape function
    args = [self, value, write]
    if isinstance(value, (str, unicode)):
        args.append(xmlrpclib_escape)

    try:
        # Try for an exact match first
        f = self.dispatch[type(value)]
    except KeyError:
        # Try for an isinstance() match
        for Type, f in self.dispatch.iteritems():
            if isinstance(value, Type):
                f(*args)
                return
        raise TypeError, "cannot marshal %s objects" % type(value)
    else:
        f(*args)


class ModXMLRPCRequestHandler(object):
    """Simple XML-RPC handler for mod_python environment"""

    def __init__(self):
        self.funcs = {}
        self.traceback = False
        #introspection functions
        self.register_function(self.ping, name="ping")
        self.register_function(self.list_api, name="_listapi")
        self.register_function(self.system_listMethods, name="system.listMethods")
        self.register_function(self.system_methodSignature, name="system.methodSignature")
        self.register_function(self.system_methodHelp, name="system.methodHelp")
        self.register_function(self.multiCall)

    def register_function(self, function, name = None):
        if name is None:
            name = function.__name__
        self.funcs[name] = function

    def register_module(self, instance, prefix=None):
        """Register all the public functions in an instance with prefix prepended

        For example
            h.register_module(exports,"pub.sys")
        will register the methods of exports with names like
            pub.sys.method1
            pub.sys.method2
            ...etc
        """
        for name in dir(instance):
            if name.startswith('_'):
                continue
            function = getattr(instance, name)
            if not callable(function):
                continue
            if prefix is not None:
                name = "%s.%s" %(prefix,name)
            self.register_function(function, name=name)

    def register_instance(self,instance):
        self.register_module(instance)

    def _marshaled_dispatch(self, data, req):
        """Dispatches an XML-RPC method from marshalled (XML) data."""

        params, method = loads(data)
        pythonopts = req.get_options()

        # Populate the Apache environment variables
        req.add_common_vars()

        opts={}
        opts['remoteuser'] = req.user

        if req.subprocess_env.get("KRB5CCNAME") is not None:
            opts['krbccache'] = req.subprocess_env.get("KRB5CCNAME")
        else:
            response = dumps(Fault(5, "Did not receive Kerberos credentials."))
            return response

        debuglevel = logging.INFO
        if pythonopts.get("IPADebug"):
            opts['ipadebug'] = pythonopts.get("IPADebug").lower()

            if opts['ipadebug'] == "on":
                debuglevel = logging.DEBUG

        if not opts.get('ipadebug'):
            opts['ipadebug'] = "off"

        logging.basicConfig(level=debuglevel,
            format='[%(asctime)s] [%(levelname)s] %(message)s',
                datefmt='%a %b %d %H:%M:%S %Y',
                stream=sys.stderr)

#        if opts['ipadebug'] == "on":
#            for o in opts:
#                logging.debug("IPA: setting option %s: %s" % (o, opts[o]))
#            for e in req.subprocess_env:
#                logging.debug("IPA: environment %s: %s" % (e, req.subprocess_env[e]))

        # Tack onto the end of the passed-in arguments any options we also
        # need
        params = params + (opts,)

        # special case
#        if method == "get_user":
#            Marshaller._Marshaller__dump = xmlrpclib_dump

        start = time.time()
        # generate response
        try:
            response = self._dispatch(method, params)
            # wrap response in a singleton tuple
            response = (response,)
            response = dumps(response, methodresponse=1, allow_none=1)
        except ipaerror.IPAError, e:
            self.traceback = True

            if (isinstance(e.detail, ldap.LDAPError) and len(e.detail[0].get('desc')) > 1):
                err = ": %s: %s" % (e.detail[0].get('desc'), e.detail[0].get('info',''))
                response = dumps(Fault(e.code, str(e) + err))
            else:
                response = dumps(Fault(e.code, str(e)))
        except:
            self.traceback = True
            # report exception back to server
            e_class, e = sys.exc_info()[:2]
            faultCode = getattr(e_class,'faultCode',1)
            tb_str = ''.join(traceback.format_exception(*sys.exc_info()))
            faultString = tb_str
            response = dumps(Fault(faultCode, faultString))

        return response

    def _dispatch(self,method,params):
        func = self.funcs.get(method,None)
        if func is None:
             raise Fault(1, "Invalid method: %s" % method)

        args = list(ipautil.unwrap_binary_data(params))
        for i in range(len(args)):
          if args[i] == '__NONE__':
              args[i] = None 

        ret = func(*args)

        return ipautil.wrap_binary_data(ret)

    def multiCall(self, calls):
        """Execute a multicall.  Execute each method call in the calls list, collecting
        results and errors, and return those as a list."""
        results = []
        for call in calls:
            try:
                result = self._dispatch(call['methodName'], call['params'])
            except Fault, fault:
                results.append({'faultCode': fault.faultCode, 'faultString': fault.faultString})
            except:
                # transform unknown exceptions into XML-RPC Faults
                # don't create a reference to full traceback since this creates
                # a circular reference.
                exc_type, exc_value = sys.exc_info()[:2]
                faultCode = getattr(exc_type, 'faultCode', 1)
                faultString = ', '.join(exc_value.args)
                trace = traceback.format_exception(*sys.exc_info())
                # traceback is not part of the multicall spec, but we include it for debugging purposes
                results.append({'faultCode': faultCode, 'faultString': faultString, 'traceback': trace})
            else:
                results.append([result])

        return results

    def list_api(self,opts):
        funcs = []
        for name,func in self.funcs.items():
            #the keys in self.funcs determine the name of the method as seen over xmlrpc
            #func.__name__ might differ (e.g. for dotted method names)
            args = self._getFuncArgs(func)
            funcs.append({'name': name,
                          'doc': func.__doc__,
                          'args': args})
        return funcs

    def ping(self,opts):
        """Simple test to see if the XML-RPC is up and active."""
        return "pong"

    def _getFuncArgs(self, func):
        args = []
        for x in range(0, func.func_code.co_argcount):
            if x == 0 and func.func_code.co_varnames[x] == "self":
                continue
            # opts is a name we tack on internally. Don't publish it.
            if func.func_code.co_varnames[x] == "opts":
                continue
            if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults):
                args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)]))
            else:
                args.append(func.func_code.co_varnames[x])
        return args

    def system_listMethods(self, opts):
        return self.funcs.keys()

    def system_methodSignature(self, method, opts):
        #it is not possible to autogenerate this data
        return 'signatures not supported'

    def system_methodHelp(self, method, opts):
        func = self.funcs.get(method)
        if func is None:
            return ""
        arglist = []
        for arg in self._getFuncArgs(func):
            if isinstance(arg,str):
                arglist.append(arg)
            else:
                arglist.append('%s=%s' % (arg[0], arg[1]))
        ret = '%s(%s)' % (method, ", ".join(arglist))
        if func.__doc__:
            ret += "\ndescription: %s" % func.__doc__
        return ret

    def handle_request(self,req):
        """Handle a single XML-RPC request"""

        # XMLRPC uses POST only. Reject anything else
        if req.method != 'POST':
            req.allow_methods(['POST'],1)
            raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED

        # The LDAP connection pool is not thread-safe. Avoid problems and
        # force the forked model for now.
        if apache.mpm_query(apache.AP_MPMQ_IS_THREADED):
            response = dumps(Fault(3, "Apache must use the forked model"))
        else:
            response = self._marshaled_dispatch(req.read(), req)

        req.content_type = "text/xml"
        req.set_content_length(len(response))
        req.write(response)


#
# mod_python handler
#

def handler(req, profiling=False):
    if profiling:
        import profile, pstats, StringIO, tempfile
        global _profiling_req
        _profiling_req = req
        temp = tempfile.NamedTemporaryFile()
        profile.run("import ipxmlrpc; ipaxmlrpc.handler(ipaxmlrpc._profiling_req, False)", temp.name)
        stats = pstats.Stats(temp.name)
        strstream = StringIO.StringIO()
        sys.stdout = strstream
        stats.sort_stats("time")
        stats.print_stats()
        req.write("<pre>" + strstream.getvalue() + "</pre>")
        _profiling_req = None
    else:
        opts = req.get_options()
        try:
            f = funcs.IPAServer()
            h = ModXMLRPCRequestHandler()
            h.register_function(f.version)
            h.register_function(f.get_aci_entry)
            h.register_function(f.get_entry_by_dn)
            h.register_function(f.get_entry_by_cn)
            h.register_function(f.update_entry)
            h.register_function(f.get_user_by_uid)
            h.register_function(f.get_user_by_principal)
            h.register_function(f.get_user_by_email)
            h.register_function(f.get_users_by_manager)
            h.register_function(f.add_user)
            h.register_function(f.get_custom_fields)
            h.register_function(f.set_custom_fields)
            h.register_function(f.get_all_users)
            h.register_function(f.find_users)
            h.register_function(f.update_user)
            h.register_function(f.delete_user)
            h.register_function(f.mark_user_active)
            h.register_function(f.mark_user_inactive)
            h.register_function(f.mark_group_active)
            h.register_function(f.mark_group_inactive)
            h.register_function(f.modifyPassword)
            h.register_function(f.get_groups_by_member)
            h.register_function(f.add_group)
            h.register_function(f.find_groups)
            h.register_function(f.add_member_to_group)
            h.register_function(f.add_members_to_group)
            h.register_function(f.remove_member_from_group)
            h.register_function(f.remove_members_from_group)
            h.register_function(f.add_user_to_group)
            h.register_function(f.add_users_to_group)
            h.register_function(f.add_group_to_group)
            h.register_function(f.remove_user_from_group)
            h.register_function(f.remove_users_from_group)
            h.register_function(f.add_groups_to_user)
            h.register_function(f.remove_groups_from_user)
            h.register_function(f.update_group)
            h.register_function(f.delete_group)
            h.register_function(f.attrs_to_labels)
            h.register_function(f.get_all_attrs)
            h.register_function(f.group_members)
            h.register_function(f.get_ipa_config)
            h.register_function(f.update_ipa_config)
            h.register_function(f.get_password_policy)
            h.register_function(f.update_password_policy)
            h.register_function(f.add_service_principal)
            h.register_function(f.delete_service_principal)
            h.register_function(f.find_service_principal)
            h.register_function(f.get_radius_client_by_ip_addr)
            h.register_function(f.add_radius_client)
            h.register_function(f.update_radius_client)
            h.register_function(f.delete_radius_client)
            h.register_function(f.find_radius_clients)
            h.register_function(f.get_radius_profile_by_uid)
            h.register_function(f.add_radius_profile)
            h.register_function(f.update_radius_profile)
            h.register_function(f.delete_radius_profile)
            h.register_function(f.find_radius_profiles)
            h.handle_request(req)
        finally:
             pass
    return apache.OK
