xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/xmlrpc/server.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
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 ([email protected]).
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 1:
724            match = pattern.search(text, here)
725            if not match: break
726            start, end = match.span()
727            results.append(escape(text[here:start]))
728
729            all, scheme, rfc, pep, selfdot, name = match.groups()
730            if scheme:
731                url = escape(all).replace('"', '"')
732                results.append('<a href="%s">%s</a>' % (url, url))
733            elif rfc:
734                url = 'https://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc)
735                results.append('<a href="%s">%s</a>' % (url, escape(all)))
736            elif pep:
737                url = 'https://peps.python.org/pep-%04d/' % int(pep)
738                results.append('<a href="%s">%s</a>' % (url, escape(all)))
739            elif text[end:end+1] == '(':
740                results.append(self.namelink(name, methods, funcs, classes))
741            elif selfdot:
742                results.append('self.<strong>%s</strong>' % name)
743            else:
744                results.append(self.namelink(name, classes))
745            here = end
746        results.append(escape(text[here:]))
747        return ''.join(results)
748
749    def docroutine(self, object, name, mod=None,
750                   funcs={}, classes={}, methods={}, cl=None):
751        """Produce HTML documentation for a function or method object."""
752
753        anchor = (cl and cl.__name__ or '') + '-' + name
754        note = ''
755
756        title = '<a name="%s"><strong>%s</strong></a>' % (
757            self.escape(anchor), self.escape(name))
758
759        if callable(object):
760            argspec = str(signature(object))
761        else:
762            argspec = '(...)'
763
764        if isinstance(object, tuple):
765            argspec = object[0] or argspec
766            docstring = object[1] or ""
767        else:
768            docstring = pydoc.getdoc(object)
769
770        decl = title + argspec + (note and self.grey(
771               '<font face="helvetica, arial">%s</font>' % note))
772
773        doc = self.markup(
774            docstring, self.preformat, funcs, classes, methods)
775        doc = doc and '<dd><tt>%s</tt></dd>' % doc
776        return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
777
778    def docserver(self, server_name, package_documentation, methods):
779        """Produce HTML documentation for an XML-RPC server."""
780
781        fdict = {}
782        for key, value in methods.items():
783            fdict[key] = '#-' + key
784            fdict[value] = fdict[key]
785
786        server_name = self.escape(server_name)
787        head = '<big><big><strong>%s</strong></big></big>' % server_name
788        result = self.heading(head)
789
790        doc = self.markup(package_documentation, self.preformat, fdict)
791        doc = doc and '<tt>%s</tt>' % doc
792        result = result + '<p>%s</p>\n' % doc
793
794        contents = []
795        method_items = sorted(methods.items())
796        for key, value in method_items:
797            contents.append(self.docroutine(value, key, funcs=fdict))
798        result = result + self.bigsection(
799            'Methods', 'functions', ''.join(contents))
800
801        return result
802
803
804    def page(self, title, contents):
805        """Format an HTML page."""
806        css_path = "/pydoc.css"
807        css_link = (
808            '<link rel="stylesheet" type="text/css" href="%s">' %
809            css_path)
810        return '''\
811<!DOCTYPE>
812<html lang="en">
813<head>
814<meta charset="utf-8">
815<title>Python: %s</title>
816%s</head><body>%s</body></html>''' % (title, css_link, contents)
817
818class XMLRPCDocGenerator:
819    """Generates documentation for an XML-RPC server.
820
821    This class is designed as mix-in and should not
822    be constructed directly.
823    """
824
825    def __init__(self):
826        # setup variables used for HTML documentation
827        self.server_name = 'XML-RPC Server Documentation'
828        self.server_documentation = \
829            "This server exports the following methods through the XML-RPC "\
830            "protocol."
831        self.server_title = 'XML-RPC Server Documentation'
832
833    def set_server_title(self, server_title):
834        """Set the HTML title of the generated server documentation"""
835
836        self.server_title = server_title
837
838    def set_server_name(self, server_name):
839        """Set the name of the generated HTML server documentation"""
840
841        self.server_name = server_name
842
843    def set_server_documentation(self, server_documentation):
844        """Set the documentation string for the entire server."""
845
846        self.server_documentation = server_documentation
847
848    def generate_html_documentation(self):
849        """generate_html_documentation() => html documentation for the server
850
851        Generates HTML documentation for the server using introspection for
852        installed functions and instances that do not implement the
853        _dispatch method. Alternatively, instances can choose to implement
854        the _get_method_argstring(method_name) method to provide the
855        argument string used in the documentation and the
856        _methodHelp(method_name) method to provide the help text used
857        in the documentation."""
858
859        methods = {}
860
861        for method_name in self.system_listMethods():
862            if method_name in self.funcs:
863                method = self.funcs[method_name]
864            elif self.instance is not None:
865                method_info = [None, None] # argspec, documentation
866                if hasattr(self.instance, '_get_method_argstring'):
867                    method_info[0] = self.instance._get_method_argstring(method_name)
868                if hasattr(self.instance, '_methodHelp'):
869                    method_info[1] = self.instance._methodHelp(method_name)
870
871                method_info = tuple(method_info)
872                if method_info != (None, None):
873                    method = method_info
874                elif not hasattr(self.instance, '_dispatch'):
875                    try:
876                        method = resolve_dotted_attribute(
877                                    self.instance,
878                                    method_name
879                                    )
880                    except AttributeError:
881                        method = method_info
882                else:
883                    method = method_info
884            else:
885                assert 0, "Could not find method in self.functions and no "\
886                          "instance installed"
887
888            methods[method_name] = method
889
890        documenter = ServerHTMLDoc()
891        documentation = documenter.docserver(
892                                self.server_name,
893                                self.server_documentation,
894                                methods
895                            )
896
897        return documenter.page(html.escape(self.server_title), documentation)
898
899class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
900    """XML-RPC and documentation request handler class.
901
902    Handles all HTTP POST requests and attempts to decode them as
903    XML-RPC requests.
904
905    Handles all HTTP GET requests and interprets them as requests
906    for documentation.
907    """
908
909    def _get_css(self, url):
910        path_here = os.path.dirname(os.path.realpath(__file__))
911        css_path = os.path.join(path_here, "..", "pydoc_data", "_pydoc.css")
912        with open(css_path, mode="rb") as fp:
913            return fp.read()
914
915    def do_GET(self):
916        """Handles the HTTP GET request.
917
918        Interpret all HTTP GET requests as requests for server
919        documentation.
920        """
921        # Check that the path is legal
922        if not self.is_rpc_path_valid():
923            self.report_404()
924            return
925
926        if self.path.endswith('.css'):
927            content_type = 'text/css'
928            response = self._get_css(self.path)
929        else:
930            content_type = 'text/html'
931            response = self.server.generate_html_documentation().encode('utf-8')
932
933        self.send_response(200)
934        self.send_header('Content-Type', '%s; charset=UTF-8' % content_type)
935        self.send_header("Content-length", str(len(response)))
936        self.end_headers()
937        self.wfile.write(response)
938
939class DocXMLRPCServer(  SimpleXMLRPCServer,
940                        XMLRPCDocGenerator):
941    """XML-RPC and HTML documentation server.
942
943    Adds the ability to serve server documentation to the capabilities
944    of SimpleXMLRPCServer.
945    """
946
947    def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler,
948                 logRequests=True, allow_none=False, encoding=None,
949                 bind_and_activate=True, use_builtin_types=False):
950        SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests,
951                                    allow_none, encoding, bind_and_activate,
952                                    use_builtin_types)
953        XMLRPCDocGenerator.__init__(self)
954
955class DocCGIXMLRPCRequestHandler(   CGIXMLRPCRequestHandler,
956                                    XMLRPCDocGenerator):
957    """Handler for XML-RPC data and documentation requests passed through
958    CGI"""
959
960    def handle_get(self):
961        """Handles the HTTP GET request.
962
963        Interpret all HTTP GET requests as requests for server
964        documentation.
965        """
966
967        response = self.generate_html_documentation().encode('utf-8')
968
969        print('Content-Type: text/html')
970        print('Content-Length: %d' % len(response))
971        print()
972        sys.stdout.flush()
973        sys.stdout.buffer.write(response)
974        sys.stdout.buffer.flush()
975
976    def __init__(self):
977        CGIXMLRPCRequestHandler.__init__(self)
978        XMLRPCDocGenerator.__init__(self)
979
980
981if __name__ == '__main__':
982    import datetime
983
984    class ExampleService:
985        def getData(self):
986            return '42'
987
988        class currentTime:
989            @staticmethod
990            def getCurrentTime():
991                return datetime.datetime.now()
992
993    with SimpleXMLRPCServer(("localhost", 8000)) as server:
994        server.register_function(pow)
995        server.register_function(lambda x,y: x+y, 'add')
996        server.register_instance(ExampleService(), allow_dotted_names=True)
997        server.register_multicall_functions()
998        print('Serving XML-RPC on localhost port 8000')
999        print('It is advisable to run this example server within a secure, closed network.')
1000        try:
1001            server.serve_forever()
1002        except KeyboardInterrupt:
1003            print("\nKeyboard interrupt received, exiting.")
1004            sys.exit(0)
1005