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