• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1r"""XML-RPC Servers.
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
11The Doc* classes can be used to create XML-RPC servers that
12serve pydoc-style documentation in response to HTTP
13GET requests. This documentation is dynamically generated
14based on the functions and methods registered with the
15server.
16
17A list of possible usage patterns follows:
18
191. Install functions:
20
21server = SimpleXMLRPCServer(("localhost", 8000))
22server.register_function(pow)
23server.register_function(lambda x,y: x+y, 'add')
24server.serve_forever()
25
262. Install an instance:
27
28class MyFuncs:
29    def __init__(self):
30        # make all of the sys functions available through sys.func_name
31        import sys
32        self.sys = sys
33    def _listMethods(self):
34        # implement this method so that system.listMethods
35        # knows to advertise the sys methods
36        return list_public_methods(self) + \
37                ['sys.' + method for method in list_public_methods(self.sys)]
38    def pow(self, x, y): return pow(x, y)
39    def add(self, x, y) : return x + y
40
41server = SimpleXMLRPCServer(("localhost", 8000))
42server.register_introspection_functions()
43server.register_instance(MyFuncs())
44server.serve_forever()
45
463. Install an instance with custom dispatch method:
47
48class Math:
49    def _listMethods(self):
50        # this method must be present for system.listMethods
51        # to work
52        return ['add', 'pow']
53    def _methodHelp(self, method):
54        # this method must be present for system.methodHelp
55        # to work
56        if method == 'add':
57            return "add(2,3) => 5"
58        elif method == 'pow':
59            return "pow(x, y[, z]) => number"
60        else:
61            # By convention, return empty
62            # string if no help is available
63            return ""
64    def _dispatch(self, method, params):
65        if method == 'pow':
66            return pow(*params)
67        elif method == 'add':
68            return params[0] + params[1]
69        else:
70            raise ValueError('bad method')
71
72server = SimpleXMLRPCServer(("localhost", 8000))
73server.register_introspection_functions()
74server.register_instance(Math())
75server.serve_forever()
76
774. Subclass SimpleXMLRPCServer:
78
79class MathServer(SimpleXMLRPCServer):
80    def _dispatch(self, method, params):
81        try:
82            # We are forcing the 'export_' prefix on methods that are
83            # callable through XML-RPC to prevent potential security
84            # problems
85            func = getattr(self, 'export_' + method)
86        except AttributeError:
87            raise Exception('method "%s" is not supported' % method)
88        else:
89            return func(*params)
90
91    def export_add(self, x, y):
92        return x + y
93
94server = MathServer(("localhost", 8000))
95server.serve_forever()
96
975. CGI script:
98
99server = CGIXMLRPCRequestHandler()
100server.register_function(pow)
101server.handle_request()
102"""
103
104# Written by Brian Quinlan (brian@sweetapp.com).
105# Based on code written by Fredrik Lundh.
106
107from xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode
108from http.server import BaseHTTPRequestHandler
109import http.server
110import socketserver
111import sys
112import os
113import re
114import pydoc
115import inspect
116import traceback
117try:
118    import fcntl
119except ImportError:
120    fcntl = None
121
122def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
123    """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
124
125    Resolves a dotted attribute name to an object.  Raises
126    an AttributeError if any attribute in the chain starts with a '_'.
127
128    If the optional allow_dotted_names argument is false, dots are not
129    supported and this function operates similar to getattr(obj, attr).
130    """
131
132    if allow_dotted_names:
133        attrs = attr.split('.')
134    else:
135        attrs = [attr]
136
137    for i in attrs:
138        if i.startswith('_'):
139            raise AttributeError(
140                'attempt to access private attribute "%s"' % i
141                )
142        else:
143            obj = getattr(obj,i)
144    return obj
145
146def list_public_methods(obj):
147    """Returns a list of attribute strings, found in the specified
148    object, which represent callable attributes"""
149
150    return [member for member in dir(obj)
151                if not member.startswith('_') and
152                    callable(getattr(obj, member))]
153
154class SimpleXMLRPCDispatcher:
155    """Mix-in class that dispatches XML-RPC requests.
156
157    This class is used to register XML-RPC method handlers
158    and then to dispatch them. This class doesn't need to be
159    instanced directly when used by SimpleXMLRPCServer but it
160    can be instanced when used by the MultiPathXMLRPCServer
161    """
162
163    def __init__(self, allow_none=False, encoding=None,
164                 use_builtin_types=False):
165        self.funcs = {}
166        self.instance = None
167        self.allow_none = allow_none
168        self.encoding = encoding or 'utf-8'
169        self.use_builtin_types = use_builtin_types
170
171    def register_instance(self, instance, allow_dotted_names=False):
172        """Registers an instance to respond to XML-RPC requests.
173
174        Only one instance can be installed at a time.
175
176        If the registered instance has a _dispatch method then that
177        method will be called with the name of the XML-RPC method and
178        its parameters as a tuple
179        e.g. instance._dispatch('add',(2,3))
180
181        If the registered instance does not have a _dispatch method
182        then the instance will be searched to find a matching method
183        and, if found, will be called. Methods beginning with an '_'
184        are considered private and will not be called by
185        SimpleXMLRPCServer.
186
187        If a registered function matches an XML-RPC request, then it
188        will be called instead of the registered instance.
189
190        If the optional allow_dotted_names argument is true and the
191        instance does not have a _dispatch method, method names
192        containing dots are supported and resolved, as long as none of
193        the name segments start with an '_'.
194
195            *** SECURITY WARNING: ***
196
197            Enabling the allow_dotted_names options allows intruders
198            to access your module's global variables and may allow
199            intruders to execute arbitrary code on your machine.  Only
200            use this option on a secure, closed network.
201
202        """
203
204        self.instance = instance
205        self.allow_dotted_names = allow_dotted_names
206
207    def register_function(self, function, name=None):
208        """Registers a function to respond to XML-RPC requests.
209
210        The optional name argument can be used to set a Unicode name
211        for the function.
212        """
213
214        if name is None:
215            name = function.__name__
216        self.funcs[name] = function
217
218    def register_introspection_functions(self):
219        """Registers the XML-RPC introspection methods in the system
220        namespace.
221
222        see http://xmlrpc.usefulinc.com/doc/reserved.html
223        """
224
225        self.funcs.update({'system.listMethods' : self.system_listMethods,
226                      'system.methodSignature' : self.system_methodSignature,
227                      'system.methodHelp' : self.system_methodHelp})
228
229    def register_multicall_functions(self):
230        """Registers the XML-RPC multicall method in the system
231        namespace.
232
233        see http://www.xmlrpc.com/discuss/msgReader$1208"""
234
235        self.funcs.update({'system.multicall' : self.system_multicall})
236
237    def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
238        """Dispatches an XML-RPC method from marshalled (XML) data.
239
240        XML-RPC methods are dispatched from the marshalled (XML) data
241        using the _dispatch method and the result is returned as
242        marshalled data. For backwards compatibility, a dispatch
243        function can be provided as an argument (see comment in
244        SimpleXMLRPCRequestHandler.do_POST) but overriding the
245        existing method through subclassing is the preferred means
246        of changing method dispatch behavior.
247        """
248
249        try:
250            params, method = loads(data, use_builtin_types=self.use_builtin_types)
251
252            # generate response
253            if dispatch_method is not None:
254                response = dispatch_method(method, params)
255            else:
256                response = self._dispatch(method, params)
257            # wrap response in a singleton tuple
258            response = (response,)
259            response = dumps(response, methodresponse=1,
260                             allow_none=self.allow_none, encoding=self.encoding)
261        except Fault as fault:
262            response = dumps(fault, allow_none=self.allow_none,
263                             encoding=self.encoding)
264        except:
265            # report exception back to server
266            exc_type, exc_value, exc_tb = sys.exc_info()
267            response = dumps(
268                Fault(1, "%s:%s" % (exc_type, exc_value)),
269                encoding=self.encoding, allow_none=self.allow_none,
270                )
271
272        return response.encode(self.encoding, 'xmlcharrefreplace')
273
274    def system_listMethods(self):
275        """system.listMethods() => ['add', 'subtract', 'multiple']
276
277        Returns a list of the methods supported by the server."""
278
279        methods = set(self.funcs.keys())
280        if self.instance is not None:
281            # Instance can implement _listMethod to return a list of
282            # methods
283            if hasattr(self.instance, '_listMethods'):
284                methods |= set(self.instance._listMethods())
285            # if the instance has a _dispatch method then we
286            # don't have enough information to provide a list
287            # of methods
288            elif not hasattr(self.instance, '_dispatch'):
289                methods |= set(list_public_methods(self.instance))
290        return sorted(methods)
291
292    def system_methodSignature(self, method_name):
293        """system.methodSignature('add') => [double, int, int]
294
295        Returns a list describing the signature of the method. In the
296        above example, the add method takes two integers as arguments
297        and returns a double result.
298
299        This server does NOT support system.methodSignature."""
300
301        # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
302
303        return 'signatures not supported'
304
305    def system_methodHelp(self, method_name):
306        """system.methodHelp('add') => "Adds two integers together"
307
308        Returns a string containing documentation for the specified method."""
309
310        method = None
311        if method_name in self.funcs:
312            method = self.funcs[method_name]
313        elif self.instance is not None:
314            # Instance can implement _methodHelp to return help for a method
315            if hasattr(self.instance, '_methodHelp'):
316                return self.instance._methodHelp(method_name)
317            # if the instance has a _dispatch method then we
318            # don't have enough information to provide help
319            elif not hasattr(self.instance, '_dispatch'):
320                try:
321                    method = resolve_dotted_attribute(
322                                self.instance,
323                                method_name,
324                                self.allow_dotted_names
325                                )
326                except AttributeError:
327                    pass
328
329        # Note that we aren't checking that the method actually
330        # be a callable object of some kind
331        if method is None:
332            return ""
333        else:
334            return pydoc.getdoc(method)
335
336    def system_multicall(self, call_list):
337        """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
338[[4], ...]
339
340        Allows the caller to package multiple XML-RPC calls into a single
341        request.
342
343        See http://www.xmlrpc.com/discuss/msgReader$1208
344        """
345
346        results = []
347        for call in call_list:
348            method_name = call['methodName']
349            params = call['params']
350
351            try:
352                # XXX A marshalling error in any response will fail the entire
353                # multicall. If someone cares they should fix this.
354                results.append([self._dispatch(method_name, params)])
355            except Fault as fault:
356                results.append(
357                    {'faultCode' : fault.faultCode,
358                     'faultString' : fault.faultString}
359                    )
360            except:
361                exc_type, exc_value, exc_tb = sys.exc_info()
362                results.append(
363                    {'faultCode' : 1,
364                     'faultString' : "%s:%s" % (exc_type, exc_value)}
365                    )
366        return results
367
368    def _dispatch(self, method, params):
369        """Dispatches the XML-RPC method.
370
371        XML-RPC calls are forwarded to a registered function that
372        matches the called XML-RPC method name. If no such function
373        exists then the call is forwarded to the registered instance,
374        if available.
375
376        If the registered instance has a _dispatch method then that
377        method will be called with the name of the XML-RPC method and
378        its parameters as a tuple
379        e.g. instance._dispatch('add',(2,3))
380
381        If the registered instance does not have a _dispatch method
382        then the instance will be searched to find a matching method
383        and, if found, will be called.
384
385        Methods beginning with an '_' are considered private and will
386        not be called.
387        """
388
389        func = None
390        try:
391            # check to see if a matching function has been registered
392            func = self.funcs[method]
393        except KeyError:
394            if self.instance is not None:
395                # check for a _dispatch method
396                if hasattr(self.instance, '_dispatch'):
397                    return self.instance._dispatch(method, params)
398                else:
399                    # call instance method directly
400                    try:
401                        func = resolve_dotted_attribute(
402                            self.instance,
403                            method,
404                            self.allow_dotted_names
405                            )
406                    except AttributeError:
407                        pass
408
409        if func is not None:
410            return func(*params)
411        else:
412            raise Exception('method "%s" is not supported' % method)
413
414class SimpleXMLRPCRequestHandler(BaseHTTPRequestHandler):
415    """Simple XML-RPC request handler class.
416
417    Handles all HTTP POST requests and attempts to decode them as
418    XML-RPC requests.
419    """
420
421    # Class attribute listing the accessible path components;
422    # paths not on this list will result in a 404 error.
423    rpc_paths = ('/', '/RPC2')
424
425    #if not None, encode responses larger than this, if possible
426    encode_threshold = 1400 #a common MTU
427
428    #Override form StreamRequestHandler: full buffering of output
429    #and no Nagle.
430    wbufsize = -1
431    disable_nagle_algorithm = True
432
433    # a re to match a gzip Accept-Encoding
434    aepattern = re.compile(r"""
435                            \s* ([^\s;]+) \s*            #content-coding
436                            (;\s* q \s*=\s* ([0-9\.]+))? #q
437                            """, re.VERBOSE | re.IGNORECASE)
438
439    def accept_encodings(self):
440        r = {}
441        ae = self.headers.get("Accept-Encoding", "")
442        for e in ae.split(","):
443            match = self.aepattern.match(e)
444            if match:
445                v = match.group(3)
446                v = float(v) if v else 1.0
447                r[match.group(1)] = v
448        return r
449
450    def is_rpc_path_valid(self):
451        if self.rpc_paths:
452            return self.path in self.rpc_paths
453        else:
454            # If .rpc_paths is empty, just assume all paths are legal
455            return True
456
457    def do_POST(self):
458        """Handles the HTTP POST request.
459
460        Attempts to interpret all HTTP POST requests as XML-RPC calls,
461        which are forwarded to the server's _dispatch method for handling.
462        """
463
464        # Check that the path is legal
465        if not self.is_rpc_path_valid():
466            self.report_404()
467            return
468
469        try:
470            # Get arguments by reading body of request.
471            # We read this in chunks to avoid straining
472            # socket.read(); around the 10 or 15Mb mark, some platforms
473            # begin to have problems (bug #792570).
474            max_chunk_size = 10*1024*1024
475            size_remaining = int(self.headers["content-length"])
476            L = []
477            while size_remaining:
478                chunk_size = min(size_remaining, max_chunk_size)
479                chunk = self.rfile.read(chunk_size)
480                if not chunk:
481                    break
482                L.append(chunk)
483                size_remaining -= len(L[-1])
484            data = b''.join(L)
485
486            data = self.decode_request_content(data)
487            if data is None:
488                return #response has been sent
489
490            # In previous versions of SimpleXMLRPCServer, _dispatch
491            # could be overridden in this class, instead of in
492            # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
493            # check to see if a subclass implements _dispatch and dispatch
494            # using that method if present.
495            response = self.server._marshaled_dispatch(
496                    data, getattr(self, '_dispatch', None), self.path
497                )
498        except Exception as e: # This should only happen if the module is buggy
499            # internal error, report as HTTP server error
500            self.send_response(500)
501
502            # Send information about the exception if requested
503            if hasattr(self.server, '_send_traceback_header') and \
504                    self.server._send_traceback_header:
505                self.send_header("X-exception", str(e))
506                trace = traceback.format_exc()
507                trace = str(trace.encode('ASCII', 'backslashreplace'), 'ASCII')
508                self.send_header("X-traceback", trace)
509
510            self.send_header("Content-length", "0")
511            self.end_headers()
512        else:
513            self.send_response(200)
514            self.send_header("Content-type", "text/xml")
515            if self.encode_threshold is not None:
516                if len(response) > self.encode_threshold:
517                    q = self.accept_encodings().get("gzip", 0)
518                    if q:
519                        try:
520                            response = gzip_encode(response)
521                            self.send_header("Content-Encoding", "gzip")
522                        except NotImplementedError:
523                            pass
524            self.send_header("Content-length", str(len(response)))
525            self.end_headers()
526            self.wfile.write(response)
527
528    def decode_request_content(self, data):
529        #support gzip encoding of request
530        encoding = self.headers.get("content-encoding", "identity").lower()
531        if encoding == "identity":
532            return data
533        if encoding == "gzip":
534            try:
535                return gzip_decode(data)
536            except NotImplementedError:
537                self.send_response(501, "encoding %r not supported" % encoding)
538            except ValueError:
539                self.send_response(400, "error decoding gzip content")
540        else:
541            self.send_response(501, "encoding %r not supported" % encoding)
542        self.send_header("Content-length", "0")
543        self.end_headers()
544
545    def report_404 (self):
546            # Report a 404 error
547        self.send_response(404)
548        response = b'No such page'
549        self.send_header("Content-type", "text/plain")
550        self.send_header("Content-length", str(len(response)))
551        self.end_headers()
552        self.wfile.write(response)
553
554    def log_request(self, code='-', size='-'):
555        """Selectively log an accepted request."""
556
557        if self.server.logRequests:
558            BaseHTTPRequestHandler.log_request(self, code, size)
559
560class SimpleXMLRPCServer(socketserver.TCPServer,
561                         SimpleXMLRPCDispatcher):
562    """Simple XML-RPC server.
563
564    Simple XML-RPC server that allows functions and a single instance
565    to be installed to handle requests. The default implementation
566    attempts to dispatch XML-RPC calls to the functions or instance
567    installed in the server. Override the _dispatch method inherited
568    from SimpleXMLRPCDispatcher to change this behavior.
569    """
570
571    allow_reuse_address = True
572
573    # Warning: this is for debugging purposes only! Never set this to True in
574    # production code, as will be sending out sensitive information (exception
575    # and stack trace details) when exceptions are raised inside
576    # SimpleXMLRPCRequestHandler.do_POST
577    _send_traceback_header = False
578
579    def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
580                 logRequests=True, allow_none=False, encoding=None,
581                 bind_and_activate=True, use_builtin_types=False):
582        self.logRequests = logRequests
583
584        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types)
585        socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
586
587
588class MultiPathXMLRPCServer(SimpleXMLRPCServer):
589    """Multipath XML-RPC Server
590    This specialization of SimpleXMLRPCServer allows the user to create
591    multiple Dispatcher instances and assign them to different
592    HTTP request paths.  This makes it possible to run two or more
593    'virtual XML-RPC servers' at the same port.
594    Make sure that the requestHandler accepts the paths in question.
595    """
596    def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
597                 logRequests=True, allow_none=False, encoding=None,
598                 bind_and_activate=True, use_builtin_types=False):
599
600        SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none,
601                                    encoding, bind_and_activate, use_builtin_types)
602        self.dispatchers = {}
603        self.allow_none = allow_none
604        self.encoding = encoding or 'utf-8'
605
606    def add_dispatcher(self, path, dispatcher):
607        self.dispatchers[path] = dispatcher
608        return dispatcher
609
610    def get_dispatcher(self, path):
611        return self.dispatchers[path]
612
613    def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
614        try:
615            response = self.dispatchers[path]._marshaled_dispatch(
616               data, dispatch_method, path)
617        except:
618            # report low level exception back to server
619            # (each dispatcher should have handled their own
620            # exceptions)
621            exc_type, exc_value = sys.exc_info()[:2]
622            response = dumps(
623                Fault(1, "%s:%s" % (exc_type, exc_value)),
624                encoding=self.encoding, allow_none=self.allow_none)
625            response = response.encode(self.encoding, 'xmlcharrefreplace')
626        return response
627
628class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher):
629    """Simple handler for XML-RPC data passed through CGI."""
630
631    def __init__(self, allow_none=False, encoding=None, use_builtin_types=False):
632        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types)
633
634    def handle_xmlrpc(self, request_text):
635        """Handle a single XML-RPC request"""
636
637        response = self._marshaled_dispatch(request_text)
638
639        print('Content-Type: text/xml')
640        print('Content-Length: %d' % len(response))
641        print()
642        sys.stdout.flush()
643        sys.stdout.buffer.write(response)
644        sys.stdout.buffer.flush()
645
646    def handle_get(self):
647        """Handle a single HTTP GET request.
648
649        Default implementation indicates an error because
650        XML-RPC uses the POST method.
651        """
652
653        code = 400
654        message, explain = BaseHTTPRequestHandler.responses[code]
655
656        response = http.server.DEFAULT_ERROR_MESSAGE % \
657            {
658             'code' : code,
659             'message' : message,
660             'explain' : explain
661            }
662        response = response.encode('utf-8')
663        print('Status: %d %s' % (code, message))
664        print('Content-Type: %s' % http.server.DEFAULT_ERROR_CONTENT_TYPE)
665        print('Content-Length: %d' % len(response))
666        print()
667        sys.stdout.flush()
668        sys.stdout.buffer.write(response)
669        sys.stdout.buffer.flush()
670
671    def handle_request(self, request_text=None):
672        """Handle a single XML-RPC request passed through a CGI post method.
673
674        If no XML data is given then it is read from stdin. The resulting
675        XML-RPC response is printed to stdout along with the correct HTTP
676        headers.
677        """
678
679        if request_text is None and \
680            os.environ.get('REQUEST_METHOD', None) == 'GET':
681            self.handle_get()
682        else:
683            # POST data is normally available through stdin
684            try:
685                length = int(os.environ.get('CONTENT_LENGTH', None))
686            except (ValueError, TypeError):
687                length = -1
688            if request_text is None:
689                request_text = sys.stdin.read(length)
690
691            self.handle_xmlrpc(request_text)
692
693
694# -----------------------------------------------------------------------------
695# Self documenting XML-RPC Server.
696
697class ServerHTMLDoc(pydoc.HTMLDoc):
698    """Class used to generate pydoc HTML document for a server"""
699
700    def markup(self, text, escape=None, funcs={}, classes={}, methods={}):
701        """Mark up some plain text, given a context of symbols to look for.
702        Each context dictionary maps object names to anchor names."""
703        escape = escape or self.escape
704        results = []
705        here = 0
706
707        # XXX Note that this regular expression does not allow for the
708        # hyperlinking of arbitrary strings being used as method
709        # names. Only methods with names consisting of word characters
710        # and '.'s are hyperlinked.
711        pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|'
712                                r'RFC[- ]?(\d+)|'
713                                r'PEP[- ]?(\d+)|'
714                                r'(self\.)?((?:\w|\.)+))\b')
715        while 1:
716            match = pattern.search(text, here)
717            if not match: break
718            start, end = match.span()
719            results.append(escape(text[here:start]))
720
721            all, scheme, rfc, pep, selfdot, name = match.groups()
722            if scheme:
723                url = escape(all).replace('"', '"')
724                results.append('<a href="%s">%s</a>' % (url, url))
725            elif rfc:
726                url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc)
727                results.append('<a href="%s">%s</a>' % (url, escape(all)))
728            elif pep:
729                url = 'http://www.python.org/dev/peps/pep-%04d/' % int(pep)
730                results.append('<a href="%s">%s</a>' % (url, escape(all)))
731            elif text[end:end+1] == '(':
732                results.append(self.namelink(name, methods, funcs, classes))
733            elif selfdot:
734                results.append('self.<strong>%s</strong>' % name)
735            else:
736                results.append(self.namelink(name, classes))
737            here = end
738        results.append(escape(text[here:]))
739        return ''.join(results)
740
741    def docroutine(self, object, name, mod=None,
742                   funcs={}, classes={}, methods={}, cl=None):
743        """Produce HTML documentation for a function or method object."""
744
745        anchor = (cl and cl.__name__ or '') + '-' + name
746        note = ''
747
748        title = '<a name="%s"><strong>%s</strong></a>' % (
749            self.escape(anchor), self.escape(name))
750
751        if inspect.ismethod(object):
752            args = inspect.getfullargspec(object)
753            # exclude the argument bound to the instance, it will be
754            # confusing to the non-Python user
755            argspec = inspect.formatargspec (
756                    args.args[1:],
757                    args.varargs,
758                    args.varkw,
759                    args.defaults,
760                    annotations=args.annotations,
761                    formatvalue=self.formatvalue
762                )
763        elif inspect.isfunction(object):
764            args = inspect.getfullargspec(object)
765            argspec = inspect.formatargspec(
766                args.args, args.varargs, args.varkw, args.defaults,
767                annotations=args.annotations,
768                formatvalue=self.formatvalue)
769        else:
770            argspec = '(...)'
771
772        if isinstance(object, tuple):
773            argspec = object[0] or argspec
774            docstring = object[1] or ""
775        else:
776            docstring = pydoc.getdoc(object)
777
778        decl = title + argspec + (note and self.grey(
779               '<font face="helvetica, arial">%s</font>' % note))
780
781        doc = self.markup(
782            docstring, self.preformat, funcs, classes, methods)
783        doc = doc and '<dd><tt>%s</tt></dd>' % doc
784        return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
785
786    def docserver(self, server_name, package_documentation, methods):
787        """Produce HTML documentation for an XML-RPC server."""
788
789        fdict = {}
790        for key, value in methods.items():
791            fdict[key] = '#-' + key
792            fdict[value] = fdict[key]
793
794        server_name = self.escape(server_name)
795        head = '<big><big><strong>%s</strong></big></big>' % server_name
796        result = self.heading(head, '#ffffff', '#7799ee')
797
798        doc = self.markup(package_documentation, self.preformat, fdict)
799        doc = doc and '<tt>%s</tt>' % doc
800        result = result + '<p>%s</p>\n' % doc
801
802        contents = []
803        method_items = sorted(methods.items())
804        for key, value in method_items:
805            contents.append(self.docroutine(value, key, funcs=fdict))
806        result = result + self.bigsection(
807            'Methods', '#ffffff', '#eeaa77', ''.join(contents))
808
809        return result
810
811class XMLRPCDocGenerator:
812    """Generates documentation for an XML-RPC server.
813
814    This class is designed as mix-in and should not
815    be constructed directly.
816    """
817
818    def __init__(self):
819        # setup variables used for HTML documentation
820        self.server_name = 'XML-RPC Server Documentation'
821        self.server_documentation = \
822            "This server exports the following methods through the XML-RPC "\
823            "protocol."
824        self.server_title = 'XML-RPC Server Documentation'
825
826    def set_server_title(self, server_title):
827        """Set the HTML title of the generated server documentation"""
828
829        self.server_title = server_title
830
831    def set_server_name(self, server_name):
832        """Set the name of the generated HTML server documentation"""
833
834        self.server_name = server_name
835
836    def set_server_documentation(self, server_documentation):
837        """Set the documentation string for the entire server."""
838
839        self.server_documentation = server_documentation
840
841    def generate_html_documentation(self):
842        """generate_html_documentation() => html documentation for the server
843
844        Generates HTML documentation for the server using introspection for
845        installed functions and instances that do not implement the
846        _dispatch method. Alternatively, instances can choose to implement
847        the _get_method_argstring(method_name) method to provide the
848        argument string used in the documentation and the
849        _methodHelp(method_name) method to provide the help text used
850        in the documentation."""
851
852        methods = {}
853
854        for method_name in self.system_listMethods():
855            if method_name in self.funcs:
856                method = self.funcs[method_name]
857            elif self.instance is not None:
858                method_info = [None, None] # argspec, documentation
859                if hasattr(self.instance, '_get_method_argstring'):
860                    method_info[0] = self.instance._get_method_argstring(method_name)
861                if hasattr(self.instance, '_methodHelp'):
862                    method_info[1] = self.instance._methodHelp(method_name)
863
864                method_info = tuple(method_info)
865                if method_info != (None, None):
866                    method = method_info
867                elif not hasattr(self.instance, '_dispatch'):
868                    try:
869                        method = resolve_dotted_attribute(
870                                    self.instance,
871                                    method_name
872                                    )
873                    except AttributeError:
874                        method = method_info
875                else:
876                    method = method_info
877            else:
878                assert 0, "Could not find method in self.functions and no "\
879                          "instance installed"
880
881            methods[method_name] = method
882
883        documenter = ServerHTMLDoc()
884        documentation = documenter.docserver(
885                                self.server_name,
886                                self.server_documentation,
887                                methods
888                            )
889
890        return documenter.page(self.server_title, documentation)
891
892class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
893    """XML-RPC and documentation request handler class.
894
895    Handles all HTTP POST requests and attempts to decode them as
896    XML-RPC requests.
897
898    Handles all HTTP GET requests and interprets them as requests
899    for documentation.
900    """
901
902    def do_GET(self):
903        """Handles the HTTP GET request.
904
905        Interpret all HTTP GET requests as requests for server
906        documentation.
907        """
908        # Check that the path is legal
909        if not self.is_rpc_path_valid():
910            self.report_404()
911            return
912
913        response = self.server.generate_html_documentation().encode('utf-8')
914        self.send_response(200)
915        self.send_header("Content-type", "text/html")
916        self.send_header("Content-length", str(len(response)))
917        self.end_headers()
918        self.wfile.write(response)
919
920class DocXMLRPCServer(  SimpleXMLRPCServer,
921                        XMLRPCDocGenerator):
922    """XML-RPC and HTML documentation server.
923
924    Adds the ability to serve server documentation to the capabilities
925    of SimpleXMLRPCServer.
926    """
927
928    def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler,
929                 logRequests=True, allow_none=False, encoding=None,
930                 bind_and_activate=True, use_builtin_types=False):
931        SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests,
932                                    allow_none, encoding, bind_and_activate,
933                                    use_builtin_types)
934        XMLRPCDocGenerator.__init__(self)
935
936class DocCGIXMLRPCRequestHandler(   CGIXMLRPCRequestHandler,
937                                    XMLRPCDocGenerator):
938    """Handler for XML-RPC data and documentation requests passed through
939    CGI"""
940
941    def handle_get(self):
942        """Handles the HTTP GET request.
943
944        Interpret all HTTP GET requests as requests for server
945        documentation.
946        """
947
948        response = self.generate_html_documentation().encode('utf-8')
949
950        print('Content-Type: text/html')
951        print('Content-Length: %d' % len(response))
952        print()
953        sys.stdout.flush()
954        sys.stdout.buffer.write(response)
955        sys.stdout.buffer.flush()
956
957    def __init__(self):
958        CGIXMLRPCRequestHandler.__init__(self)
959        XMLRPCDocGenerator.__init__(self)
960
961
962if __name__ == '__main__':
963    import datetime
964
965    class ExampleService:
966        def getData(self):
967            return '42'
968
969        class currentTime:
970            @staticmethod
971            def getCurrentTime():
972                return datetime.datetime.now()
973
974    with SimpleXMLRPCServer(("localhost", 8000)) as server:
975        server.register_function(pow)
976        server.register_function(lambda x,y: x+y, 'add')
977        server.register_instance(ExampleService(), allow_dotted_names=True)
978        server.register_multicall_functions()
979        print('Serving XML-RPC on localhost port 8000')
980        print('It is advisable to run this example server within a secure, closed network.')
981        try:
982            server.serve_forever()
983        except KeyboardInterrupt:
984            print("\nKeyboard interrupt received, exiting.")
985            sys.exit(0)
986