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