• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""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 a 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                L.append(self.rfile.read(chunk_size))
490                size_remaining -= len(L[-1])
491            data = ''.join(L)
492
493            data = self.decode_request_content(data)
494            if data is None:
495                return #response has been sent
496
497            # In previous versions of SimpleXMLRPCServer, _dispatch
498            # could be overridden in this class, instead of in
499            # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
500            # check to see if a subclass implements _dispatch and dispatch
501            # using that method if present.
502            response = self.server._marshaled_dispatch(
503                    data, getattr(self, '_dispatch', None), self.path
504                )
505        except Exception, e: # This should only happen if the module is buggy
506            # internal error, report as HTTP server error
507            self.send_response(500)
508
509            # Send information about the exception if requested
510            if hasattr(self.server, '_send_traceback_header') and \
511                    self.server._send_traceback_header:
512                self.send_header("X-exception", str(e))
513                self.send_header("X-traceback", traceback.format_exc())
514
515            self.send_header("Content-length", "0")
516            self.end_headers()
517        else:
518            # got a valid XML RPC response
519            self.send_response(200)
520            self.send_header("Content-type", "text/xml")
521            if self.encode_threshold is not None:
522                if len(response) > self.encode_threshold:
523                    q = self.accept_encodings().get("gzip", 0)
524                    if q:
525                        try:
526                            response = xmlrpclib.gzip_encode(response)
527                            self.send_header("Content-Encoding", "gzip")
528                        except NotImplementedError:
529                            pass
530            self.send_header("Content-length", str(len(response)))
531            self.end_headers()
532            self.wfile.write(response)
533
534    def decode_request_content(self, data):
535        #support gzip encoding of request
536        encoding = self.headers.get("content-encoding", "identity").lower()
537        if encoding == "identity":
538            return data
539        if encoding == "gzip":
540            try:
541                return xmlrpclib.gzip_decode(data)
542            except NotImplementedError:
543                self.send_response(501, "encoding %r not supported" % encoding)
544            except ValueError:
545                self.send_response(400, "error decoding gzip content")
546        else:
547            self.send_response(501, "encoding %r not supported" % encoding)
548        self.send_header("Content-length", "0")
549        self.end_headers()
550
551    def report_404 (self):
552            # Report a 404 error
553        self.send_response(404)
554        response = 'No such page'
555        self.send_header("Content-type", "text/plain")
556        self.send_header("Content-length", str(len(response)))
557        self.end_headers()
558        self.wfile.write(response)
559
560    def log_request(self, code='-', size='-'):
561        """Selectively log an accepted request."""
562
563        if self.server.logRequests:
564            BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
565
566class SimpleXMLRPCServer(SocketServer.TCPServer,
567                         SimpleXMLRPCDispatcher):
568    """Simple XML-RPC server.
569
570    Simple XML-RPC server that allows functions and a single instance
571    to be installed to handle requests. The default implementation
572    attempts to dispatch XML-RPC calls to the functions or instance
573    installed in the server. Override the _dispatch method inhereted
574    from SimpleXMLRPCDispatcher to change this behavior.
575    """
576
577    allow_reuse_address = True
578
579    # Warning: this is for debugging purposes only! Never set this to True in
580    # production code, as will be sending out sensitive information (exception
581    # and stack trace details) when exceptions are raised inside
582    # SimpleXMLRPCRequestHandler.do_POST
583    _send_traceback_header = False
584
585    def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
586                 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
587        self.logRequests = logRequests
588
589        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
590        SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
591
592        # [Bug #1222790] If possible, set close-on-exec flag; if a
593        # method spawns a subprocess, the subprocess shouldn't have
594        # the listening socket open.
595        if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
596            flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
597            flags |= fcntl.FD_CLOEXEC
598            fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
599
600class MultiPathXMLRPCServer(SimpleXMLRPCServer):
601    """Multipath XML-RPC Server
602    This specialization of SimpleXMLRPCServer allows the user to create
603    multiple Dispatcher instances and assign them to different
604    HTTP request paths.  This makes it possible to run two or more
605    'virtual XML-RPC servers' at the same port.
606    Make sure that the requestHandler accepts the paths in question.
607    """
608    def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
609                 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
610
611        SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none,
612                                    encoding, bind_and_activate)
613        self.dispatchers = {}
614        self.allow_none = allow_none
615        self.encoding = encoding
616
617    def add_dispatcher(self, path, dispatcher):
618        self.dispatchers[path] = dispatcher
619        return dispatcher
620
621    def get_dispatcher(self, path):
622        return self.dispatchers[path]
623
624    def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
625        try:
626            response = self.dispatchers[path]._marshaled_dispatch(
627               data, dispatch_method, path)
628        except:
629            # report low level exception back to server
630            # (each dispatcher should have handled their own
631            # exceptions)
632            exc_type, exc_value = sys.exc_info()[:2]
633            response = xmlrpclib.dumps(
634                xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
635                encoding=self.encoding, allow_none=self.allow_none)
636        return response
637
638class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher):
639    """Simple handler for XML-RPC data passed through CGI."""
640
641    def __init__(self, allow_none=False, encoding=None):
642        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
643
644    def handle_xmlrpc(self, request_text):
645        """Handle a single XML-RPC request"""
646
647        response = self._marshaled_dispatch(request_text)
648
649        print 'Content-Type: text/xml'
650        print 'Content-Length: %d' % len(response)
651        print
652        sys.stdout.write(response)
653
654    def handle_get(self):
655        """Handle a single HTTP GET request.
656
657        Default implementation indicates an error because
658        XML-RPC uses the POST method.
659        """
660
661        code = 400
662        message, explain = \
663                 BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
664
665        response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
666            {
667             'code' : code,
668             'message' : message,
669             'explain' : explain
670            }
671        print 'Status: %d %s' % (code, message)
672        print 'Content-Type: %s' % BaseHTTPServer.DEFAULT_ERROR_CONTENT_TYPE
673        print 'Content-Length: %d' % len(response)
674        print
675        sys.stdout.write(response)
676
677    def handle_request(self, request_text = None):
678        """Handle a single XML-RPC request passed through a CGI post method.
679
680        If no XML data is given then it is read from stdin. The resulting
681        XML-RPC response is printed to stdout along with the correct HTTP
682        headers.
683        """
684
685        if request_text is None and \
686            os.environ.get('REQUEST_METHOD', None) == 'GET':
687            self.handle_get()
688        else:
689            # POST data is normally available through stdin
690            try:
691                length = int(os.environ.get('CONTENT_LENGTH', None))
692            except (TypeError, ValueError):
693                length = -1
694            if request_text is None:
695                request_text = sys.stdin.read(length)
696
697            self.handle_xmlrpc(request_text)
698
699if __name__ == '__main__':
700    print 'Running XML-RPC server on port 8000'
701    server = SimpleXMLRPCServer(("localhost", 8000))
702    server.register_function(pow)
703    server.register_function(lambda x,y: x+y, 'add')
704    server.serve_forever()
705