• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1r"""Simple XML-RPC Server.
2
3This module can be used to create simple XML-RPC servers
4by creating a server and either installing functions, a
5class instance, or by extending the SimpleXMLRPCServer
6class.
7
8It can also be used to handle XML-RPC requests in a CGI
9environment using CGIXMLRPCRequestHandler.
10
11A list of possible usage patterns follows:
12
131. Install functions:
14
15server = SimpleXMLRPCServer(("localhost", 8000))
16server.register_function(pow)
17server.register_function(lambda x,y: x+y, 'add')
18server.serve_forever()
19
202. Install an instance:
21
22class MyFuncs:
23    def __init__(self):
24        # make all of the string functions available through
25        # string.func_name
26        import string
27        self.string = string
28    def _listMethods(self):
29        # implement this method so that system.listMethods
30        # knows to advertise the strings methods
31        return list_public_methods(self) + \
32                ['string.' + method for method in list_public_methods(self.string)]
33    def pow(self, x, y): return pow(x, y)
34    def add(self, x, y) : return x + y
35
36server = SimpleXMLRPCServer(("localhost", 8000))
37server.register_introspection_functions()
38server.register_instance(MyFuncs())
39server.serve_forever()
40
413. Install an instance with custom dispatch method:
42
43class Math:
44    def _listMethods(self):
45        # this method must be present for system.listMethods
46        # to work
47        return ['add', 'pow']
48    def _methodHelp(self, method):
49        # this method must be present for system.methodHelp
50        # to work
51        if method == 'add':
52            return "add(2,3) => 5"
53        elif method == 'pow':
54            return "pow(x, y[, z]) => number"
55        else:
56            # By convention, return empty
57            # string if no help is available
58            return ""
59    def _dispatch(self, method, params):
60        if method == 'pow':
61            return pow(*params)
62        elif method == 'add':
63            return params[0] + params[1]
64        else:
65            raise 'bad method'
66
67server = SimpleXMLRPCServer(("localhost", 8000))
68server.register_introspection_functions()
69server.register_instance(Math())
70server.serve_forever()
71
724. Subclass SimpleXMLRPCServer:
73
74class MathServer(SimpleXMLRPCServer):
75    def _dispatch(self, method, params):
76        try:
77            # We are forcing the 'export_' prefix on methods that are
78            # callable through XML-RPC to prevent potential security
79            # problems
80            func = getattr(self, 'export_' + method)
81        except AttributeError:
82            raise Exception('method "%s" is not supported' % method)
83        else:
84            return func(*params)
85
86    def export_add(self, x, y):
87        return x + y
88
89server = MathServer(("localhost", 8000))
90server.serve_forever()
91
925. CGI script:
93
94server = CGIXMLRPCRequestHandler()
95server.register_function(pow)
96server.handle_request()
97"""
98
99# Written by Brian Quinlan (brian@sweetapp.com).
100# Based on code written by Fredrik Lundh.
101
102import xmlrpclib
103from xmlrpclib import Fault
104import SocketServer
105import BaseHTTPServer
106import sys
107import os
108import traceback
109import re
110try:
111    import fcntl
112except ImportError:
113    fcntl = None
114
115def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
116    """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
117
118    Resolves a dotted attribute name to an object.  Raises
119    an AttributeError if any attribute in the chain starts with a '_'.
120
121    If the optional allow_dotted_names argument is false, dots are not
122    supported and this function operates similar to getattr(obj, attr).
123    """
124
125    if allow_dotted_names:
126        attrs = attr.split('.')
127    else:
128        attrs = [attr]
129
130    for i in attrs:
131        if i.startswith('_'):
132            raise AttributeError(
133                'attempt to access private attribute "%s"' % i
134                )
135        else:
136            obj = getattr(obj,i)
137    return obj
138
139def list_public_methods(obj):
140    """Returns a list of attribute strings, found in the specified
141    object, which represent callable attributes"""
142
143    return [member for member in dir(obj)
144                if not member.startswith('_') and
145                    hasattr(getattr(obj, member), '__call__')]
146
147def remove_duplicates(lst):
148    """remove_duplicates([2,2,2,1,3,3]) => [3,1,2]
149
150    Returns a copy of a list without duplicates. Every list
151    item must be hashable and the order of the items in the
152    resulting list is not defined.
153    """
154    u = {}
155    for x in lst:
156        u[x] = 1
157
158    return u.keys()
159
160class SimpleXMLRPCDispatcher:
161    """Mix-in class that dispatches XML-RPC requests.
162
163    This class is used to register XML-RPC method handlers
164    and then to dispatch them. This class doesn't need to be
165    instanced directly when used by SimpleXMLRPCServer but it
166    can be instanced when used by the MultiPathXMLRPCServer.
167    """
168
169    def __init__(self, allow_none=False, encoding=None):
170        self.funcs = {}
171        self.instance = None
172        self.allow_none = allow_none
173        self.encoding = encoding
174
175    def register_instance(self, instance, allow_dotted_names=False):
176        """Registers an instance to respond to XML-RPC requests.
177
178        Only one instance can be installed at a time.
179
180        If the registered instance has a _dispatch method then that
181        method will be called with the name of the XML-RPC method and
182        its parameters as a tuple
183        e.g. instance._dispatch('add',(2,3))
184
185        If the registered instance does not have a _dispatch method
186        then the instance will be searched to find a matching method
187        and, if found, will be called. Methods beginning with an '_'
188        are considered private and will not be called by
189        SimpleXMLRPCServer.
190
191        If a registered function matches an XML-RPC request, then it
192        will be called instead of the registered instance.
193
194        If the optional allow_dotted_names argument is true and the
195        instance does not have a _dispatch method, method names
196        containing dots are supported and resolved, as long as none of
197        the name segments start with an '_'.
198
199            *** SECURITY WARNING: ***
200
201            Enabling the allow_dotted_names options allows intruders
202            to access your module's global variables and may allow
203            intruders to execute arbitrary code on your machine.  Only
204            use this option on a secure, closed network.
205
206        """
207
208        self.instance = instance
209        self.allow_dotted_names = allow_dotted_names
210
211    def register_function(self, function, name = None):
212        """Registers a function to respond to XML-RPC requests.
213
214        The optional name argument can be used to set a Unicode name
215        for the function.
216        """
217
218        if name is None:
219            name = function.__name__
220        self.funcs[name] = function
221
222    def register_introspection_functions(self):
223        """Registers the XML-RPC introspection methods in the system
224        namespace.
225
226        see http://xmlrpc.usefulinc.com/doc/reserved.html
227        """
228
229        self.funcs.update({'system.listMethods' : self.system_listMethods,
230                      'system.methodSignature' : self.system_methodSignature,
231                      'system.methodHelp' : self.system_methodHelp})
232
233    def register_multicall_functions(self):
234        """Registers the XML-RPC multicall method in the system
235        namespace.
236
237        see http://www.xmlrpc.com/discuss/msgReader$1208"""
238
239        self.funcs.update({'system.multicall' : self.system_multicall})
240
241    def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
242        """Dispatches an XML-RPC method from marshalled (XML) data.
243
244        XML-RPC methods are dispatched from the marshalled (XML) data
245        using the _dispatch method and the result is returned as
246        marshalled data. For backwards compatibility, a dispatch
247        function can be provided as an argument (see comment in
248        SimpleXMLRPCRequestHandler.do_POST) but overriding the
249        existing method through subclassing is the preferred means
250        of changing method dispatch behavior.
251        """
252
253        try:
254            params, method = xmlrpclib.loads(data)
255
256            # generate response
257            if dispatch_method is not None:
258                response = dispatch_method(method, params)
259            else:
260                response = self._dispatch(method, params)
261            # wrap response in a singleton tuple
262            response = (response,)
263            response = xmlrpclib.dumps(response, methodresponse=1,
264                                       allow_none=self.allow_none, encoding=self.encoding)
265        except Fault, fault:
266            response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
267                                       encoding=self.encoding)
268        except:
269            # report exception back to server
270            exc_type, exc_value, exc_tb = sys.exc_info()
271            response = xmlrpclib.dumps(
272                xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
273                encoding=self.encoding, allow_none=self.allow_none,
274                )
275
276        return response
277
278    def system_listMethods(self):
279        """system.listMethods() => ['add', 'subtract', 'multiple']
280
281        Returns a list of the methods supported by the server."""
282
283        methods = self.funcs.keys()
284        if self.instance is not None:
285            # Instance can implement _listMethod to return a list of
286            # methods
287            if hasattr(self.instance, '_listMethods'):
288                methods = remove_duplicates(
289                        methods + self.instance._listMethods()
290                    )
291            # if the instance has a _dispatch method then we
292            # don't have enough information to provide a list
293            # of methods
294            elif not hasattr(self.instance, '_dispatch'):
295                methods = remove_duplicates(
296                        methods + list_public_methods(self.instance)
297                    )
298        methods.sort()
299        return methods
300
301    def system_methodSignature(self, method_name):
302        """system.methodSignature('add') => [double, int, int]
303
304        Returns a list describing the signature of the method. In the
305        above example, the add method takes two integers as arguments
306        and returns a double result.
307
308        This server does NOT support system.methodSignature."""
309
310        # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
311
312        return 'signatures not supported'
313
314    def system_methodHelp(self, method_name):
315        """system.methodHelp('add') => "Adds two integers together"
316
317        Returns a string containing documentation for the specified method."""
318
319        method = None
320        if method_name in self.funcs:
321            method = self.funcs[method_name]
322        elif self.instance is not None:
323            # Instance can implement _methodHelp to return help for a method
324            if hasattr(self.instance, '_methodHelp'):
325                return self.instance._methodHelp(method_name)
326            # if the instance has a _dispatch method then we
327            # don't have enough information to provide help
328            elif not hasattr(self.instance, '_dispatch'):
329                try:
330                    method = resolve_dotted_attribute(
331                                self.instance,
332                                method_name,
333                                self.allow_dotted_names
334                                )
335                except AttributeError:
336                    pass
337
338        # Note that we aren't checking that the method actually
339        # be a callable object of some kind
340        if method is None:
341            return ""
342        else:
343            import pydoc
344            return pydoc.getdoc(method)
345
346    def system_multicall(self, call_list):
347        """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
348[[4], ...]
349
350        Allows the caller to package multiple XML-RPC calls into a single
351        request.
352
353        See http://www.xmlrpc.com/discuss/msgReader$1208
354        """
355
356        results = []
357        for call in call_list:
358            method_name = call['methodName']
359            params = call['params']
360
361            try:
362                # XXX A marshalling error in any response will fail the entire
363                # multicall. If someone cares they should fix this.
364                results.append([self._dispatch(method_name, params)])
365            except Fault, fault:
366                results.append(
367                    {'faultCode' : fault.faultCode,
368                     'faultString' : fault.faultString}
369                    )
370            except:
371                exc_type, exc_value, exc_tb = sys.exc_info()
372                results.append(
373                    {'faultCode' : 1,
374                     'faultString' : "%s:%s" % (exc_type, exc_value)}
375                    )
376        return results
377
378    def _dispatch(self, method, params):
379        """Dispatches the XML-RPC method.
380
381        XML-RPC calls are forwarded to a registered function that
382        matches the called XML-RPC method name. If no such function
383        exists then the call is forwarded to the registered instance,
384        if available.
385
386        If the registered instance has a _dispatch method then that
387        method will be called with the name of the XML-RPC method and
388        its parameters as a tuple
389        e.g. instance._dispatch('add',(2,3))
390
391        If the registered instance does not have a _dispatch method
392        then the instance will be searched to find a matching method
393        and, if found, will be called.
394
395        Methods beginning with an '_' are considered private and will
396        not be called.
397        """
398
399        func = None
400        try:
401            # check to see if a matching function has been registered
402            func = self.funcs[method]
403        except KeyError:
404            if self.instance is not None:
405                # check for a _dispatch method
406                if hasattr(self.instance, '_dispatch'):
407                    return self.instance._dispatch(method, params)
408                else:
409                    # call instance method directly
410                    try:
411                        func = resolve_dotted_attribute(
412                            self.instance,
413                            method,
414                            self.allow_dotted_names
415                            )
416                    except AttributeError:
417                        pass
418
419        if func is not None:
420            return func(*params)
421        else:
422            raise Exception('method "%s" is not supported' % method)
423
424class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
425    """Simple XML-RPC request handler class.
426
427    Handles all HTTP POST requests and attempts to decode them as
428    XML-RPC requests.
429    """
430
431    # Class attribute listing the accessible path components;
432    # paths not on this list will result in a 404 error.
433    rpc_paths = ('/', '/RPC2')
434
435    #if not None, encode responses larger than this, if possible
436    encode_threshold = 1400 #a common MTU
437
438    #Override form StreamRequestHandler: full buffering of output
439    #and no Nagle.
440    wbufsize = -1
441    disable_nagle_algorithm = True
442
443    # a re to match a gzip Accept-Encoding
444    aepattern = re.compile(r"""
445                            \s* ([^\s;]+) \s*            #content-coding
446                            (;\s* q \s*=\s* ([0-9\.]+))? #q
447                            """, re.VERBOSE | re.IGNORECASE)
448
449    def accept_encodings(self):
450        r = {}
451        ae = self.headers.get("Accept-Encoding", "")
452        for e in ae.split(","):
453            match = self.aepattern.match(e)
454            if match:
455                v = match.group(3)
456                v = float(v) if v else 1.0
457                r[match.group(1)] = v
458        return r
459
460    def is_rpc_path_valid(self):
461        if self.rpc_paths:
462            return self.path in self.rpc_paths
463        else:
464            # If .rpc_paths is empty, just assume all paths are legal
465            return True
466
467    def do_POST(self):
468        """Handles the HTTP POST request.
469
470        Attempts to interpret all HTTP POST requests as XML-RPC calls,
471        which are forwarded to the server's _dispatch method for handling.
472        """
473
474        # Check that the path is legal
475        if not self.is_rpc_path_valid():
476            self.report_404()
477            return
478
479        try:
480            # Get arguments by reading body of request.
481            # We read this in chunks to avoid straining
482            # socket.read(); around the 10 or 15Mb mark, some platforms
483            # begin to have problems (bug #792570).
484            max_chunk_size = 10*1024*1024
485            size_remaining = int(self.headers["content-length"])
486            L = []
487            while size_remaining:
488                chunk_size = min(size_remaining, max_chunk_size)
489                chunk = self.rfile.read(chunk_size)
490                if not chunk:
491                    break
492                L.append(chunk)
493                size_remaining -= len(L[-1])
494            data = ''.join(L)
495
496            data = self.decode_request_content(data)
497            if data is None:
498                return #response has been sent
499
500            # In previous versions of SimpleXMLRPCServer, _dispatch
501            # could be overridden in this class, instead of in
502            # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
503            # check to see if a subclass implements _dispatch and dispatch
504            # using that method if present.
505            response = self.server._marshaled_dispatch(
506                    data, getattr(self, '_dispatch', None), self.path
507                )
508        except Exception, e: # This should only happen if the module is buggy
509            # internal error, report as HTTP server error
510            self.send_response(500)
511
512            # Send information about the exception if requested
513            if hasattr(self.server, '_send_traceback_header') and \
514                    self.server._send_traceback_header:
515                self.send_header("X-exception", str(e))
516                self.send_header("X-traceback", traceback.format_exc())
517
518            self.send_header("Content-length", "0")
519            self.end_headers()
520        else:
521            # got a valid XML RPC response
522            self.send_response(200)
523            self.send_header("Content-type", "text/xml")
524            if self.encode_threshold is not None:
525                if len(response) > self.encode_threshold:
526                    q = self.accept_encodings().get("gzip", 0)
527                    if q:
528                        try:
529                            response = xmlrpclib.gzip_encode(response)
530                            self.send_header("Content-Encoding", "gzip")
531                        except NotImplementedError:
532                            pass
533            self.send_header("Content-length", str(len(response)))
534            self.end_headers()
535            self.wfile.write(response)
536
537    def decode_request_content(self, data):
538        #support gzip encoding of request
539        encoding = self.headers.get("content-encoding", "identity").lower()
540        if encoding == "identity":
541            return data
542        if encoding == "gzip":
543            try:
544                return xmlrpclib.gzip_decode(data)
545            except NotImplementedError:
546                self.send_response(501, "encoding %r not supported" % encoding)
547            except ValueError:
548                self.send_response(400, "error decoding gzip content")
549        else:
550            self.send_response(501, "encoding %r not supported" % encoding)
551        self.send_header("Content-length", "0")
552        self.end_headers()
553
554    def report_404 (self):
555            # Report a 404 error
556        self.send_response(404)
557        response = 'No such page'
558        self.send_header("Content-type", "text/plain")
559        self.send_header("Content-length", str(len(response)))
560        self.end_headers()
561        self.wfile.write(response)
562
563    def log_request(self, code='-', size='-'):
564        """Selectively log an accepted request."""
565
566        if self.server.logRequests:
567            BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
568
569class SimpleXMLRPCServer(SocketServer.TCPServer,
570                         SimpleXMLRPCDispatcher):
571    """Simple XML-RPC server.
572
573    Simple XML-RPC server that allows functions and a single instance
574    to be installed to handle requests. The default implementation
575    attempts to dispatch XML-RPC calls to the functions or instance
576    installed in the server. Override the _dispatch method inhereted
577    from SimpleXMLRPCDispatcher to change this behavior.
578    """
579
580    allow_reuse_address = True
581
582    # Warning: this is for debugging purposes only! Never set this to True in
583    # production code, as will be sending out sensitive information (exception
584    # and stack trace details) when exceptions are raised inside
585    # SimpleXMLRPCRequestHandler.do_POST
586    _send_traceback_header = False
587
588    def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
589                 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
590        self.logRequests = logRequests
591
592        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
593        SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
594
595        # [Bug #1222790] If possible, set close-on-exec flag; if a
596        # method spawns a subprocess, the subprocess shouldn't have
597        # the listening socket open.
598        if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
599            flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
600            flags |= fcntl.FD_CLOEXEC
601            fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
602
603class MultiPathXMLRPCServer(SimpleXMLRPCServer):
604    """Multipath XML-RPC Server
605    This specialization of SimpleXMLRPCServer allows the user to create
606    multiple Dispatcher instances and assign them to different
607    HTTP request paths.  This makes it possible to run two or more
608    'virtual XML-RPC servers' at the same port.
609    Make sure that the requestHandler accepts the paths in question.
610    """
611    def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
612                 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
613
614        SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none,
615                                    encoding, bind_and_activate)
616        self.dispatchers = {}
617        self.allow_none = allow_none
618        self.encoding = encoding
619
620    def add_dispatcher(self, path, dispatcher):
621        self.dispatchers[path] = dispatcher
622        return dispatcher
623
624    def get_dispatcher(self, path):
625        return self.dispatchers[path]
626
627    def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
628        try:
629            response = self.dispatchers[path]._marshaled_dispatch(
630               data, dispatch_method, path)
631        except:
632            # report low level exception back to server
633            # (each dispatcher should have handled their own
634            # exceptions)
635            exc_type, exc_value = sys.exc_info()[:2]
636            response = xmlrpclib.dumps(
637                xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
638                encoding=self.encoding, allow_none=self.allow_none)
639        return response
640
641class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher):
642    """Simple handler for XML-RPC data passed through CGI."""
643
644    def __init__(self, allow_none=False, encoding=None):
645        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
646
647    def handle_xmlrpc(self, request_text):
648        """Handle a single XML-RPC request"""
649
650        response = self._marshaled_dispatch(request_text)
651
652        print 'Content-Type: text/xml'
653        print 'Content-Length: %d' % len(response)
654        print
655        sys.stdout.write(response)
656
657    def handle_get(self):
658        """Handle a single HTTP GET request.
659
660        Default implementation indicates an error because
661        XML-RPC uses the POST method.
662        """
663
664        code = 400
665        message, explain = \
666                 BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
667
668        response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
669            {
670             'code' : code,
671             'message' : message,
672             'explain' : explain
673            }
674        print 'Status: %d %s' % (code, message)
675        print 'Content-Type: %s' % BaseHTTPServer.DEFAULT_ERROR_CONTENT_TYPE
676        print 'Content-Length: %d' % len(response)
677        print
678        sys.stdout.write(response)
679
680    def handle_request(self, request_text = None):
681        """Handle a single XML-RPC request passed through a CGI post method.
682
683        If no XML data is given then it is read from stdin. The resulting
684        XML-RPC response is printed to stdout along with the correct HTTP
685        headers.
686        """
687
688        if request_text is None and \
689            os.environ.get('REQUEST_METHOD', None) == 'GET':
690            self.handle_get()
691        else:
692            # POST data is normally available through stdin
693            try:
694                length = int(os.environ.get('CONTENT_LENGTH', None))
695            except (TypeError, ValueError):
696                length = -1
697            if request_text is None:
698                request_text = sys.stdin.read(length)
699
700            self.handle_xmlrpc(request_text)
701
702if __name__ == '__main__':
703    print 'Running XML-RPC server on port 8000'
704    server = SimpleXMLRPCServer(("localhost", 8000))
705    server.register_function(pow)
706    server.register_function(lambda x,y: x+y, 'add')
707    server.register_multicall_functions()
708    server.serve_forever()
709