bundled SOAPpy-0.12.5
[p2pool.git] / SOAPpy / Server.py
diff --git a/SOAPpy/Server.py b/SOAPpy/Server.py
new file mode 100644 (file)
index 0000000..a01a105
--- /dev/null
@@ -0,0 +1,706 @@
+from __future__ import nested_scopes
+
+"""
+################################################################################
+#
+# SOAPpy - Cayce Ullman       (cayce@actzero.com)
+#          Brian Matthews     (blm@actzero.com)
+#          Gregory Warnes     (Gregory.R.Warnes@Pfizer.com)
+#          Christopher Blunck (blunck@gst.com)
+#
+################################################################################
+# Copyright (c) 2003, Pfizer
+# Copyright (c) 2001, Cayce Ullman.
+# Copyright (c) 2001, Brian Matthews.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 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.
+#
+# Neither the name of actzero, inc. nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 REGENTS 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.
+#
+################################################################################
+"""
+
+ident = '$Id: Server.py 1468 2008-05-24 01:55:33Z warnes $'
+from version import __version__
+
+#import xml.sax
+import socket
+import sys
+import SocketServer
+from types import *
+import BaseHTTPServer
+import thread
+
+# SOAPpy modules
+from Parser      import parseSOAPRPC
+from Config      import Config
+from Types       import faultType, voidType, simplify
+from NS          import NS
+from SOAPBuilder import buildSOAP
+from Utilities   import debugHeader, debugFooter
+
+try: from M2Crypto import SSL
+except: pass
+
+ident = '$Id: Server.py 1468 2008-05-24 01:55:33Z warnes $'
+
+from version import __version__
+
+################################################################################
+# Call context dictionary
+################################################################################
+
+_contexts = dict()
+
+def GetSOAPContext():
+    global _contexts
+    return _contexts[thread.get_ident()]
+
+################################################################################
+# Server
+################################################################################
+
+# Method Signature class for adding extra info to registered funcs, right now
+# used just to indicate it should be called with keywords, instead of ordered
+# params.
+class MethodSig:
+    def __init__(self, func, keywords=0, context=0):
+        self.func     = func
+        self.keywords = keywords
+        self.context  = context
+        self.__name__ = func.__name__
+
+    def __call__(self, *args, **kw):
+        return apply(self.func,args,kw)
+
+class SOAPContext:
+    def __init__(self, header, body, attrs, xmldata, connection, httpheaders,
+        soapaction):
+
+        self.header     = header
+        self.body       = body
+        self.attrs      = attrs
+        self.xmldata    = xmldata
+        self.connection = connection
+        self.httpheaders= httpheaders
+        self.soapaction = soapaction
+
+# A class to describe how header messages are handled
+class HeaderHandler:
+    # Initially fail out if there are any problems.
+    def __init__(self, header, attrs):
+        for i in header.__dict__.keys():
+            if i[0] == "_":
+                continue
+
+            d = getattr(header, i)
+
+            try:
+                fault = int(attrs[id(d)][(NS.ENV, 'mustUnderstand')])
+            except:
+                fault = 0
+
+            if fault:
+                raise faultType, ("%s:MustUnderstand" % NS.ENV_T,
+                                  "Required Header Misunderstood",
+                                  "%s" % i)
+
+################################################################################
+# SOAP Server
+################################################################################
+class SOAPServerBase:
+
+    def get_request(self):
+        sock, addr = SocketServer.TCPServer.get_request(self)
+
+        if self.ssl_context:
+            sock = SSL.Connection(self.ssl_context, sock)
+            sock._setup_ssl(addr)
+            if sock.accept_ssl() != 1:
+                raise socket.error, "Couldn't accept SSL connection"
+
+        return sock, addr
+
+    def registerObject(self, object, namespace = '', path = ''):
+        if namespace == '' and path == '': namespace = self.namespace
+        if namespace == '' and path != '':
+            namespace = path.replace("/", ":")
+            if namespace[0] == ":": namespace = namespace[1:]
+        self.objmap[namespace] = object
+
+    def registerFunction(self, function, namespace = '', funcName = None,
+                         path = ''):
+        if not funcName : funcName = function.__name__
+        if namespace == '' and path == '': namespace = self.namespace
+        if namespace == '' and path != '':
+            namespace = path.replace("/", ":")
+            if namespace[0] == ":": namespace = namespace[1:]
+        if self.funcmap.has_key(namespace):
+            self.funcmap[namespace][funcName] = function
+        else:
+            self.funcmap[namespace] = {funcName : function}
+
+    def registerKWObject(self, object, namespace = '', path = ''):
+        if namespace == '' and path == '': namespace = self.namespace
+        if namespace == '' and path != '':
+            namespace = path.replace("/", ":")
+            if namespace[0] == ":": namespace = namespace[1:]
+        for i in dir(object.__class__):
+            if i[0] != "_" and callable(getattr(object, i)):
+                self.registerKWFunction(getattr(object,i), namespace)
+
+    # convenience  - wraps your func for you.
+    def registerKWFunction(self, function, namespace = '', funcName = None,
+                           path = ''):
+        if namespace == '' and path == '': namespace = self.namespace
+        if namespace == '' and path != '':
+            namespace = path.replace("/", ":")
+            if namespace[0] == ":": namespace = namespace[1:]
+        self.registerFunction(MethodSig(function,keywords=1), namespace,
+        funcName)
+
+    def unregisterObject(self, object, namespace = '', path = ''):
+        if namespace == '' and path == '': namespace = self.namespace
+        if namespace == '' and path != '':
+            namespace = path.replace("/", ":")
+            if namespace[0] == ":": namespace = namespace[1:]
+
+        del self.objmap[namespace]
+        
+class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+    def version_string(self):
+        return '<a href="http://pywebsvcs.sf.net">' + \
+            'SOAPpy ' + __version__ + '</a> (Python ' + \
+            sys.version.split()[0] + ')'
+
+    def date_time_string(self):
+        self.__last_date_time_string = \
+            BaseHTTPServer.BaseHTTPRequestHandler.\
+            date_time_string(self)
+
+        return self.__last_date_time_string
+
+    def do_POST(self):
+        global _contexts
+        
+        status = 500
+        try:
+            if self.server.config.dumpHeadersIn:
+                s = 'Incoming HTTP headers'
+                debugHeader(s)
+                print self.raw_requestline.strip()
+                print "\n".join(map (lambda x: x.strip(),
+                    self.headers.headers))
+                debugFooter(s)
+
+            data = self.rfile.read(int(self.headers["Content-length"]))
+
+            if self.server.config.dumpSOAPIn:
+                s = 'Incoming SOAP'
+                debugHeader(s)
+                print data,
+                if data[-1] != '\n':
+                    print
+                debugFooter(s)
+
+            (r, header, body, attrs) = \
+                parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
+
+            method = r._name
+            args   = r._aslist()
+            kw     = r._asdict()
+
+            if Config.simplify_objects:
+                args = simplify(args)
+                kw = simplify(kw)
+
+            # Handle mixed named and unnamed arguments by assuming
+            # that all arguments with names of the form "v[0-9]+"
+            # are unnamed and should be passed in numeric order,
+            # other arguments are named and should be passed using
+            # this name.
+
+            # This is a non-standard exension to the SOAP protocol,
+            # but is supported by Apache AXIS.
+
+            # It is enabled by default.  To disable, set
+            # Config.specialArgs to False.
+
+
+            ordered_args = {}
+            named_args   = {}
+
+            if Config.specialArgs: 
+                
+                for (k,v) in  kw.items():
+
+                    if k[0]=="v":
+                        try:
+                            i = int(k[1:])
+                            ordered_args[i] = v
+                        except ValueError:
+                            named_args[str(k)] = v
+
+                    else:
+                        named_args[str(k)] = v
+
+            # We have to decide namespace precedence
+            # I'm happy with the following scenario
+            # if r._ns is specified use it, if not check for
+            # a path, if it's specified convert it and use it as the
+            # namespace. If both are specified, use r._ns.
+            
+            ns = r._ns
+
+            if len(self.path) > 1 and not ns:
+                ns = self.path.replace("/", ":")
+                if ns[0] == ":": ns = ns[1:]
+            
+            # authorization method
+            a = None
+
+            keylist = ordered_args.keys()
+            keylist.sort()
+
+            # create list in proper order w/o names
+            tmp = map( lambda x: ordered_args[x], keylist)
+            ordered_args = tmp
+
+            #print '<-> Argument Matching Yielded:'
+            #print '<-> Ordered Arguments:' + str(ordered_args)
+            #print '<-> Named Arguments  :' + str(named_args)
+             
+            resp = ""
+            
+            # For fault messages
+            if ns:
+                nsmethod = "%s:%s" % (ns, method)
+            else:
+                nsmethod = method
+
+            try:
+                # First look for registered functions
+                if self.server.funcmap.has_key(ns) and \
+                    self.server.funcmap[ns].has_key(method):
+                    f = self.server.funcmap[ns][method]
+
+                    # look for the authorization method
+                    if self.server.config.authMethod != None:
+                        authmethod = self.server.config.authMethod
+                        if self.server.funcmap.has_key(ns) and \
+                               self.server.funcmap[ns].has_key(authmethod):
+                            a = self.server.funcmap[ns][authmethod]
+                else:
+                    # Now look at registered objects
+                    # Check for nested attributes. This works even if
+                    # there are none, because the split will return
+                    # [method]
+                    f = self.server.objmap[ns]
+                    
+                    # Look for the authorization method
+                    if self.server.config.authMethod != None:
+                        authmethod = self.server.config.authMethod
+                        if hasattr(f, authmethod):
+                            a = getattr(f, authmethod)
+
+                    # then continue looking for the method
+                    l = method.split(".")
+                    for i in l:
+                        f = getattr(f, i)
+            except:
+                info = sys.exc_info()
+                try:
+                    resp = buildSOAP(faultType("%s:Client" % NS.ENV_T,
+                                               "Method Not Found",
+                                               "%s : %s %s %s" % (nsmethod,
+                                                                  info[0],
+                                                                  info[1],
+                                                                  info[2])),
+                                     encoding = self.server.encoding,
+                                     config = self.server.config)
+                finally:
+                    del info
+                status = 500
+            else:
+                try:
+                    if header:
+                        x = HeaderHandler(header, attrs)
+
+                    fr = 1
+
+                    # call context book keeping
+                    # We're stuffing the method into the soapaction if there
+                    # isn't one, someday, we'll set that on the client
+                    # and it won't be necessary here
+                    # for now we're doing both
+
+                    if "SOAPAction".lower() not in self.headers.keys() or \
+                       self.headers["SOAPAction"] == "\"\"":
+                        self.headers["SOAPAction"] = method
+                        
+                    thread_id = thread.get_ident()
+                    _contexts[thread_id] = SOAPContext(header, body,
+                                                       attrs, data,
+                                                       self.connection,
+                                                       self.headers,
+                                                       self.headers["SOAPAction"])
+
+                    # Do an authorization check
+                    if a != None:
+                        if not apply(a, (), {"_SOAPContext" :
+                                             _contexts[thread_id] }):
+                            raise faultType("%s:Server" % NS.ENV_T,
+                                            "Authorization failed.",
+                                            "%s" % nsmethod)
+                    
+                    # If it's wrapped, some special action may be needed
+                    if isinstance(f, MethodSig):
+                        c = None
+                    
+                        if f.context:  # retrieve context object
+                            c = _contexts[thread_id]
+
+                        if Config.specialArgs:
+                            if c:
+                                named_args["_SOAPContext"] = c
+                            fr = apply(f, ordered_args, named_args)
+                        elif f.keywords:
+                            # This is lame, but have to de-unicode
+                            # keywords
+                            
+                            strkw = {}
+                            
+                            for (k, v) in kw.items():
+                                strkw[str(k)] = v
+                            if c:
+                                strkw["_SOAPContext"] = c
+                            fr = apply(f, (), strkw)
+                        elif c:
+                            fr = apply(f, args, {'_SOAPContext':c})
+                        else:
+                            fr = apply(f, args, {})
+
+                    else:
+                        if Config.specialArgs:
+                            fr = apply(f, ordered_args, named_args)
+                        else:
+                            fr = apply(f, args, {})
+
+                    
+                    if type(fr) == type(self) and \
+                        isinstance(fr, voidType):
+                        resp = buildSOAP(kw = {'%sResponse' % method: fr},
+                            encoding = self.server.encoding,
+                            config = self.server.config)
+                    else:
+                        resp = buildSOAP(kw =
+                            {'%sResponse' % method: {'Result': fr}},
+                            encoding = self.server.encoding,
+                            config = self.server.config)
+
+                    # Clean up _contexts
+                    if _contexts.has_key(thread_id):
+                        del _contexts[thread_id]
+                        
+                except Exception, e:
+                    import traceback
+                    info = sys.exc_info()
+
+                    try:
+                        if self.server.config.dumpFaultInfo:
+                            s = 'Method %s exception' % nsmethod
+                            debugHeader(s)
+                            traceback.print_exception(info[0], info[1],
+                                                      info[2])
+                            debugFooter(s)
+
+                        if isinstance(e, faultType):
+                            f = e
+                        else:
+                            f = faultType("%s:Server" % NS.ENV_T,
+                                          "Method Failed",
+                                          "%s" % nsmethod)
+
+                        if self.server.config.returnFaultInfo:
+                            f._setDetail("".join(traceback.format_exception(
+                                info[0], info[1], info[2])))
+                        elif not hasattr(f, 'detail'):
+                            f._setDetail("%s %s" % (info[0], info[1]))
+                    finally:
+                        del info
+
+                    resp = buildSOAP(f, encoding = self.server.encoding,
+                       config = self.server.config)
+                    status = 500
+                else:
+                    status = 200
+        except faultType, e:
+            import traceback
+            info = sys.exc_info()
+            try:
+                if self.server.config.dumpFaultInfo:
+                    s = 'Received fault exception'
+                    debugHeader(s)
+                    traceback.print_exception(info[0], info[1],
+                        info[2])
+                    debugFooter(s)
+
+                if self.server.config.returnFaultInfo:
+                    e._setDetail("".join(traceback.format_exception(
+                            info[0], info[1], info[2])))
+                elif not hasattr(e, 'detail'):
+                    e._setDetail("%s %s" % (info[0], info[1]))
+            finally:
+                del info
+
+            resp = buildSOAP(e, encoding = self.server.encoding,
+                config = self.server.config)
+            status = 500
+        except Exception, e:
+            # internal error, report as HTTP server error
+
+            if self.server.config.dumpFaultInfo:
+                s = 'Internal exception %s' % e
+                import traceback
+                debugHeader(s)
+                info = sys.exc_info()
+                try:
+                    traceback.print_exception(info[0], info[1], info[2])
+                finally:
+                    del info
+
+                debugFooter(s)
+
+            self.send_response(500)
+            self.end_headers()
+
+            if self.server.config.dumpHeadersOut and \
+                self.request_version != 'HTTP/0.9':
+                s = 'Outgoing HTTP headers'
+                debugHeader(s)
+                if self.responses.has_key(status):
+                    s = ' ' + self.responses[status][0]
+                else:
+                    s = ''
+                print "%s %d%s" % (self.protocol_version, 500, s)
+                print "Server:", self.version_string()
+                print "Date:", self.__last_date_time_string
+                debugFooter(s)
+        else:
+            # got a valid SOAP response
+            self.send_response(status)
+
+            t = 'text/xml';
+            if self.server.encoding != None:
+                t += '; charset=%s' % self.server.encoding
+            self.send_header("Content-type", t)
+            self.send_header("Content-length", str(len(resp)))
+            self.end_headers()
+
+            if self.server.config.dumpHeadersOut and \
+                self.request_version != 'HTTP/0.9':
+                s = 'Outgoing HTTP headers'
+                debugHeader(s)
+                if self.responses.has_key(status):
+                    s = ' ' + self.responses[status][0]
+                else:
+                    s = ''
+                print "%s %d%s" % (self.protocol_version, status, s)
+                print "Server:", self.version_string()
+                print "Date:", self.__last_date_time_string
+                print "Content-type:", t
+                print "Content-length:", len(resp)
+                debugFooter(s)
+
+            if self.server.config.dumpSOAPOut:
+                s = 'Outgoing SOAP'
+                debugHeader(s)
+                print resp,
+                if resp[-1] != '\n':
+                    print
+                debugFooter(s)
+
+            self.wfile.write(resp)
+            self.wfile.flush()
+
+            # We should be able to shut down both a regular and an SSL
+            # connection, but under Python 2.1, calling shutdown on an
+            # SSL connections drops the output, so this work-around.
+            # This should be investigated more someday.
+
+            if self.server.config.SSLserver and \
+                isinstance(self.connection, SSL.Connection):
+                self.connection.set_shutdown(SSL.SSL_SENT_SHUTDOWN |
+                    SSL.SSL_RECEIVED_SHUTDOWN)
+            else:
+                self.connection.shutdown(1)
+
+        def do_GET(self):
+            
+            #print 'command        ', self.command
+            #print 'path           ', self.path
+            #print 'request_version', self.request_version
+            #print 'headers'
+            #print '   type    ', self.headers.type
+            #print '   maintype', self.headers.maintype
+            #print '   subtype ', self.headers.subtype
+            #print '   params  ', self.headers.plist
+            
+            path = self.path.lower()
+            if path.endswith('wsdl'):
+                method = 'wsdl'
+                function = namespace = None
+                if self.server.funcmap.has_key(namespace) \
+                        and self.server.funcmap[namespace].has_key(method):
+                    function = self.server.funcmap[namespace][method]
+                else: 
+                    if namespace in self.server.objmap.keys():
+                        function = self.server.objmap[namespace]
+                        l = method.split(".")
+                        for i in l:
+                            function = getattr(function, i)
+            
+                if function:
+                    self.send_response(200)
+                    self.send_header("Content-type", 'text/plain')
+                    self.end_headers()
+                    response = apply(function, ())
+                    self.wfile.write(str(response))
+                    return
+            
+            # return error
+            self.send_response(200)
+            self.send_header("Content-type", 'text/html')
+            self.end_headers()
+            self.wfile.write('''\
+<title>
+<head>Error!</head>
+</title>
+
+<body>
+<h1>Oops!</h1>
+
+<p>
+  This server supports HTTP GET requests only for the the purpose of
+  obtaining Web Services Description Language (WSDL) for a specific
+  service.
+
+  Either you requested an URL that does not end in "wsdl" or this
+  server does not implement a wsdl method.
+</p>
+
+
+</body>''')
+
+            
+    def log_message(self, format, *args):
+        if self.server.log:
+            BaseHTTPServer.BaseHTTPRequestHandler.\
+                log_message (self, format, *args)
+
+
+
+class SOAPServer(SOAPServerBase, SocketServer.TCPServer):
+
+    def __init__(self, addr = ('localhost', 8000),
+        RequestHandler = SOAPRequestHandler, log = 0, encoding = 'UTF-8',
+        config = Config, namespace = None, ssl_context = None):
+
+        # Test the encoding, raising an exception if it's not known
+        if encoding != None:
+            ''.encode(encoding)
+
+        if ssl_context != None and not config.SSLserver:
+            raise AttributeError, \
+                "SSL server not supported by this Python installation"
+
+        self.namespace          = namespace
+        self.objmap             = {}
+        self.funcmap            = {}
+        self.ssl_context        = ssl_context
+        self.encoding           = encoding
+        self.config             = config
+        self.log                = log
+
+        self.allow_reuse_address= 1
+
+        SocketServer.TCPServer.__init__(self, addr, RequestHandler)
+
+
+class ThreadingSOAPServer(SOAPServerBase, SocketServer.ThreadingTCPServer):
+
+    def __init__(self, addr = ('localhost', 8000),
+        RequestHandler = SOAPRequestHandler, log = 0, encoding = 'UTF-8',
+        config = Config, namespace = None, ssl_context = None):
+
+        # Test the encoding, raising an exception if it's not known
+        if encoding != None:
+            ''.encode(encoding)
+
+        if ssl_context != None and not config.SSLserver:
+            raise AttributeError, \
+                "SSL server not supported by this Python installation"
+
+        self.namespace          = namespace
+        self.objmap             = {}
+        self.funcmap            = {}
+        self.ssl_context        = ssl_context
+        self.encoding           = encoding
+        self.config             = config
+        self.log                = log
+
+        self.allow_reuse_address= 1
+
+        SocketServer.ThreadingTCPServer.__init__(self, addr, RequestHandler)
+
+# only define class if Unix domain sockets are available
+if hasattr(socket, "AF_UNIX"):
+
+    class SOAPUnixSocketServer(SOAPServerBase, SocketServer.UnixStreamServer):
+    
+        def __init__(self, addr = 8000,
+            RequestHandler = SOAPRequestHandler, log = 0, encoding = 'UTF-8',
+            config = Config, namespace = None, ssl_context = None):
+    
+            # Test the encoding, raising an exception if it's not known
+            if encoding != None:
+                ''.encode(encoding)
+    
+            if ssl_context != None and not config.SSLserver:
+                raise AttributeError, \
+                    "SSL server not supported by this Python installation"
+    
+            self.namespace          = namespace
+            self.objmap             = {}
+            self.funcmap            = {}
+            self.ssl_context        = ssl_context
+            self.encoding           = encoding
+            self.config             = config
+            self.log                = log
+    
+            self.allow_reuse_address= 1
+    
+            SocketServer.UnixStreamServer.__init__(self, str(addr), RequestHandler)
+