jpayne@68: r"""XML-RPC Servers. jpayne@68: jpayne@68: This module can be used to create simple XML-RPC servers jpayne@68: by creating a server and either installing functions, a jpayne@68: class instance, or by extending the SimpleXMLRPCServer jpayne@68: class. jpayne@68: jpayne@68: It can also be used to handle XML-RPC requests in a CGI jpayne@68: environment using CGIXMLRPCRequestHandler. jpayne@68: jpayne@68: The Doc* classes can be used to create XML-RPC servers that jpayne@68: serve pydoc-style documentation in response to HTTP jpayne@68: GET requests. This documentation is dynamically generated jpayne@68: based on the functions and methods registered with the jpayne@68: server. jpayne@68: jpayne@68: A list of possible usage patterns follows: jpayne@68: jpayne@68: 1. Install functions: jpayne@68: jpayne@68: server = SimpleXMLRPCServer(("localhost", 8000)) jpayne@68: server.register_function(pow) jpayne@68: server.register_function(lambda x,y: x+y, 'add') jpayne@68: server.serve_forever() jpayne@68: jpayne@68: 2. Install an instance: jpayne@68: jpayne@68: class MyFuncs: jpayne@68: def __init__(self): jpayne@68: # make all of the sys functions available through sys.func_name jpayne@68: import sys jpayne@68: self.sys = sys jpayne@68: def _listMethods(self): jpayne@68: # implement this method so that system.listMethods jpayne@68: # knows to advertise the sys methods jpayne@68: return list_public_methods(self) + \ jpayne@68: ['sys.' + method for method in list_public_methods(self.sys)] jpayne@68: def pow(self, x, y): return pow(x, y) jpayne@68: def add(self, x, y) : return x + y jpayne@68: jpayne@68: server = SimpleXMLRPCServer(("localhost", 8000)) jpayne@68: server.register_introspection_functions() jpayne@68: server.register_instance(MyFuncs()) jpayne@68: server.serve_forever() jpayne@68: jpayne@68: 3. Install an instance with custom dispatch method: jpayne@68: jpayne@68: class Math: jpayne@68: def _listMethods(self): jpayne@68: # this method must be present for system.listMethods jpayne@68: # to work jpayne@68: return ['add', 'pow'] jpayne@68: def _methodHelp(self, method): jpayne@68: # this method must be present for system.methodHelp jpayne@68: # to work jpayne@68: if method == 'add': jpayne@68: return "add(2,3) => 5" jpayne@68: elif method == 'pow': jpayne@68: return "pow(x, y[, z]) => number" jpayne@68: else: jpayne@68: # By convention, return empty jpayne@68: # string if no help is available jpayne@68: return "" jpayne@68: def _dispatch(self, method, params): jpayne@68: if method == 'pow': jpayne@68: return pow(*params) jpayne@68: elif method == 'add': jpayne@68: return params[0] + params[1] jpayne@68: else: jpayne@68: raise ValueError('bad method') jpayne@68: jpayne@68: server = SimpleXMLRPCServer(("localhost", 8000)) jpayne@68: server.register_introspection_functions() jpayne@68: server.register_instance(Math()) jpayne@68: server.serve_forever() jpayne@68: jpayne@68: 4. Subclass SimpleXMLRPCServer: jpayne@68: jpayne@68: class MathServer(SimpleXMLRPCServer): jpayne@68: def _dispatch(self, method, params): jpayne@68: try: jpayne@68: # We are forcing the 'export_' prefix on methods that are jpayne@68: # callable through XML-RPC to prevent potential security jpayne@68: # problems jpayne@68: func = getattr(self, 'export_' + method) jpayne@68: except AttributeError: jpayne@68: raise Exception('method "%s" is not supported' % method) jpayne@68: else: jpayne@68: return func(*params) jpayne@68: jpayne@68: def export_add(self, x, y): jpayne@68: return x + y jpayne@68: jpayne@68: server = MathServer(("localhost", 8000)) jpayne@68: server.serve_forever() jpayne@68: jpayne@68: 5. CGI script: jpayne@68: jpayne@68: server = CGIXMLRPCRequestHandler() jpayne@68: server.register_function(pow) jpayne@68: server.handle_request() jpayne@68: """ jpayne@68: jpayne@68: # Written by Brian Quinlan (brian@sweetapp.com). jpayne@68: # Based on code written by Fredrik Lundh. jpayne@68: jpayne@68: from xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode jpayne@68: from http.server import BaseHTTPRequestHandler jpayne@68: from functools import partial jpayne@68: from inspect import signature jpayne@68: import html jpayne@68: import http.server jpayne@68: import socketserver jpayne@68: import sys jpayne@68: import os jpayne@68: import re jpayne@68: import pydoc jpayne@68: import traceback jpayne@68: try: jpayne@68: import fcntl jpayne@68: except ImportError: jpayne@68: fcntl = None jpayne@68: jpayne@68: def resolve_dotted_attribute(obj, attr, allow_dotted_names=True): jpayne@68: """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d jpayne@68: jpayne@68: Resolves a dotted attribute name to an object. Raises jpayne@68: an AttributeError if any attribute in the chain starts with a '_'. jpayne@68: jpayne@68: If the optional allow_dotted_names argument is false, dots are not jpayne@68: supported and this function operates similar to getattr(obj, attr). jpayne@68: """ jpayne@68: jpayne@68: if allow_dotted_names: jpayne@68: attrs = attr.split('.') jpayne@68: else: jpayne@68: attrs = [attr] jpayne@68: jpayne@68: for i in attrs: jpayne@68: if i.startswith('_'): jpayne@68: raise AttributeError( jpayne@68: 'attempt to access private attribute "%s"' % i jpayne@68: ) jpayne@68: else: jpayne@68: obj = getattr(obj,i) jpayne@68: return obj jpayne@68: jpayne@68: def list_public_methods(obj): jpayne@68: """Returns a list of attribute strings, found in the specified jpayne@68: object, which represent callable attributes""" jpayne@68: jpayne@68: return [member for member in dir(obj) jpayne@68: if not member.startswith('_') and jpayne@68: callable(getattr(obj, member))] jpayne@68: jpayne@68: class SimpleXMLRPCDispatcher: jpayne@68: """Mix-in class that dispatches XML-RPC requests. jpayne@68: jpayne@68: This class is used to register XML-RPC method handlers jpayne@68: and then to dispatch them. This class doesn't need to be jpayne@68: instanced directly when used by SimpleXMLRPCServer but it jpayne@68: can be instanced when used by the MultiPathXMLRPCServer jpayne@68: """ jpayne@68: jpayne@68: def __init__(self, allow_none=False, encoding=None, jpayne@68: use_builtin_types=False): jpayne@68: self.funcs = {} jpayne@68: self.instance = None jpayne@68: self.allow_none = allow_none jpayne@68: self.encoding = encoding or 'utf-8' jpayne@68: self.use_builtin_types = use_builtin_types jpayne@68: jpayne@68: def register_instance(self, instance, allow_dotted_names=False): jpayne@68: """Registers an instance to respond to XML-RPC requests. jpayne@68: jpayne@68: Only one instance can be installed at a time. jpayne@68: jpayne@68: If the registered instance has a _dispatch method then that jpayne@68: method will be called with the name of the XML-RPC method and jpayne@68: its parameters as a tuple jpayne@68: e.g. instance._dispatch('add',(2,3)) jpayne@68: jpayne@68: If the registered instance does not have a _dispatch method jpayne@68: then the instance will be searched to find a matching method jpayne@68: and, if found, will be called. Methods beginning with an '_' jpayne@68: are considered private and will not be called by jpayne@68: SimpleXMLRPCServer. jpayne@68: jpayne@68: If a registered function matches an XML-RPC request, then it jpayne@68: will be called instead of the registered instance. jpayne@68: jpayne@68: If the optional allow_dotted_names argument is true and the jpayne@68: instance does not have a _dispatch method, method names jpayne@68: containing dots are supported and resolved, as long as none of jpayne@68: the name segments start with an '_'. jpayne@68: jpayne@68: *** SECURITY WARNING: *** jpayne@68: jpayne@68: Enabling the allow_dotted_names options allows intruders jpayne@68: to access your module's global variables and may allow jpayne@68: intruders to execute arbitrary code on your machine. Only jpayne@68: use this option on a secure, closed network. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: self.instance = instance jpayne@68: self.allow_dotted_names = allow_dotted_names jpayne@68: jpayne@68: def register_function(self, function=None, name=None): jpayne@68: """Registers a function to respond to XML-RPC requests. jpayne@68: jpayne@68: The optional name argument can be used to set a Unicode name jpayne@68: for the function. jpayne@68: """ jpayne@68: # decorator factory jpayne@68: if function is None: jpayne@68: return partial(self.register_function, name=name) jpayne@68: jpayne@68: if name is None: jpayne@68: name = function.__name__ jpayne@68: self.funcs[name] = function jpayne@68: jpayne@68: return function jpayne@68: jpayne@68: def register_introspection_functions(self): jpayne@68: """Registers the XML-RPC introspection methods in the system jpayne@68: namespace. jpayne@68: jpayne@68: see http://xmlrpc.usefulinc.com/doc/reserved.html jpayne@68: """ jpayne@68: jpayne@68: self.funcs.update({'system.listMethods' : self.system_listMethods, jpayne@68: 'system.methodSignature' : self.system_methodSignature, jpayne@68: 'system.methodHelp' : self.system_methodHelp}) jpayne@68: jpayne@68: def register_multicall_functions(self): jpayne@68: """Registers the XML-RPC multicall method in the system jpayne@68: namespace. jpayne@68: jpayne@68: see http://www.xmlrpc.com/discuss/msgReader$1208""" jpayne@68: jpayne@68: self.funcs.update({'system.multicall' : self.system_multicall}) jpayne@68: jpayne@68: def _marshaled_dispatch(self, data, dispatch_method = None, path = None): jpayne@68: """Dispatches an XML-RPC method from marshalled (XML) data. jpayne@68: jpayne@68: XML-RPC methods are dispatched from the marshalled (XML) data jpayne@68: using the _dispatch method and the result is returned as jpayne@68: marshalled data. For backwards compatibility, a dispatch jpayne@68: function can be provided as an argument (see comment in jpayne@68: SimpleXMLRPCRequestHandler.do_POST) but overriding the jpayne@68: existing method through subclassing is the preferred means jpayne@68: of changing method dispatch behavior. jpayne@68: """ jpayne@68: jpayne@68: try: jpayne@68: params, method = loads(data, use_builtin_types=self.use_builtin_types) jpayne@68: jpayne@68: # generate response jpayne@68: if dispatch_method is not None: jpayne@68: response = dispatch_method(method, params) jpayne@68: else: jpayne@68: response = self._dispatch(method, params) jpayne@68: # wrap response in a singleton tuple jpayne@68: response = (response,) jpayne@68: response = dumps(response, methodresponse=1, jpayne@68: allow_none=self.allow_none, encoding=self.encoding) jpayne@68: except Fault as fault: jpayne@68: response = dumps(fault, allow_none=self.allow_none, jpayne@68: encoding=self.encoding) jpayne@68: except: jpayne@68: # report exception back to server jpayne@68: exc_type, exc_value, exc_tb = sys.exc_info() jpayne@68: try: jpayne@68: response = dumps( jpayne@68: Fault(1, "%s:%s" % (exc_type, exc_value)), jpayne@68: encoding=self.encoding, allow_none=self.allow_none, jpayne@68: ) jpayne@68: finally: jpayne@68: # Break reference cycle jpayne@68: exc_type = exc_value = exc_tb = None jpayne@68: jpayne@68: return response.encode(self.encoding, 'xmlcharrefreplace') jpayne@68: jpayne@68: def system_listMethods(self): jpayne@68: """system.listMethods() => ['add', 'subtract', 'multiple'] jpayne@68: jpayne@68: Returns a list of the methods supported by the server.""" jpayne@68: jpayne@68: methods = set(self.funcs.keys()) jpayne@68: if self.instance is not None: jpayne@68: # Instance can implement _listMethod to return a list of jpayne@68: # methods jpayne@68: if hasattr(self.instance, '_listMethods'): jpayne@68: methods |= set(self.instance._listMethods()) jpayne@68: # if the instance has a _dispatch method then we jpayne@68: # don't have enough information to provide a list jpayne@68: # of methods jpayne@68: elif not hasattr(self.instance, '_dispatch'): jpayne@68: methods |= set(list_public_methods(self.instance)) jpayne@68: return sorted(methods) jpayne@68: jpayne@68: def system_methodSignature(self, method_name): jpayne@68: """system.methodSignature('add') => [double, int, int] jpayne@68: jpayne@68: Returns a list describing the signature of the method. In the jpayne@68: above example, the add method takes two integers as arguments jpayne@68: and returns a double result. jpayne@68: jpayne@68: This server does NOT support system.methodSignature.""" jpayne@68: jpayne@68: # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html jpayne@68: jpayne@68: return 'signatures not supported' jpayne@68: jpayne@68: def system_methodHelp(self, method_name): jpayne@68: """system.methodHelp('add') => "Adds two integers together" jpayne@68: jpayne@68: Returns a string containing documentation for the specified method.""" jpayne@68: jpayne@68: method = None jpayne@68: if method_name in self.funcs: jpayne@68: method = self.funcs[method_name] jpayne@68: elif self.instance is not None: jpayne@68: # Instance can implement _methodHelp to return help for a method jpayne@68: if hasattr(self.instance, '_methodHelp'): jpayne@68: return self.instance._methodHelp(method_name) jpayne@68: # if the instance has a _dispatch method then we jpayne@68: # don't have enough information to provide help jpayne@68: elif not hasattr(self.instance, '_dispatch'): jpayne@68: try: jpayne@68: method = resolve_dotted_attribute( jpayne@68: self.instance, jpayne@68: method_name, jpayne@68: self.allow_dotted_names jpayne@68: ) jpayne@68: except AttributeError: jpayne@68: pass jpayne@68: jpayne@68: # Note that we aren't checking that the method actually jpayne@68: # be a callable object of some kind jpayne@68: if method is None: jpayne@68: return "" jpayne@68: else: jpayne@68: return pydoc.getdoc(method) jpayne@68: jpayne@68: def system_multicall(self, call_list): jpayne@68: """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \ jpayne@68: [[4], ...] jpayne@68: jpayne@68: Allows the caller to package multiple XML-RPC calls into a single jpayne@68: request. jpayne@68: jpayne@68: See http://www.xmlrpc.com/discuss/msgReader$1208 jpayne@68: """ jpayne@68: jpayne@68: results = [] jpayne@68: for call in call_list: jpayne@68: method_name = call['methodName'] jpayne@68: params = call['params'] jpayne@68: jpayne@68: try: jpayne@68: # XXX A marshalling error in any response will fail the entire jpayne@68: # multicall. If someone cares they should fix this. jpayne@68: results.append([self._dispatch(method_name, params)]) jpayne@68: except Fault as fault: jpayne@68: results.append( jpayne@68: {'faultCode' : fault.faultCode, jpayne@68: 'faultString' : fault.faultString} jpayne@68: ) jpayne@68: except: jpayne@68: exc_type, exc_value, exc_tb = sys.exc_info() jpayne@68: try: jpayne@68: results.append( jpayne@68: {'faultCode' : 1, jpayne@68: 'faultString' : "%s:%s" % (exc_type, exc_value)} jpayne@68: ) jpayne@68: finally: jpayne@68: # Break reference cycle jpayne@68: exc_type = exc_value = exc_tb = None jpayne@68: return results jpayne@68: jpayne@68: def _dispatch(self, method, params): jpayne@68: """Dispatches the XML-RPC method. jpayne@68: jpayne@68: XML-RPC calls are forwarded to a registered function that jpayne@68: matches the called XML-RPC method name. If no such function jpayne@68: exists then the call is forwarded to the registered instance, jpayne@68: if available. jpayne@68: jpayne@68: If the registered instance has a _dispatch method then that jpayne@68: method will be called with the name of the XML-RPC method and jpayne@68: its parameters as a tuple jpayne@68: e.g. instance._dispatch('add',(2,3)) jpayne@68: jpayne@68: If the registered instance does not have a _dispatch method jpayne@68: then the instance will be searched to find a matching method jpayne@68: and, if found, will be called. jpayne@68: jpayne@68: Methods beginning with an '_' are considered private and will jpayne@68: not be called. jpayne@68: """ jpayne@68: jpayne@68: try: jpayne@68: # call the matching registered function jpayne@68: func = self.funcs[method] jpayne@68: except KeyError: jpayne@68: pass jpayne@68: else: jpayne@68: if func is not None: jpayne@68: return func(*params) jpayne@68: raise Exception('method "%s" is not supported' % method) jpayne@68: jpayne@68: if self.instance is not None: jpayne@68: if hasattr(self.instance, '_dispatch'): jpayne@68: # call the `_dispatch` method on the instance jpayne@68: return self.instance._dispatch(method, params) jpayne@68: jpayne@68: # call the instance's method directly jpayne@68: try: jpayne@68: func = resolve_dotted_attribute( jpayne@68: self.instance, jpayne@68: method, jpayne@68: self.allow_dotted_names jpayne@68: ) jpayne@68: except AttributeError: jpayne@68: pass jpayne@68: else: jpayne@68: if func is not None: jpayne@68: return func(*params) jpayne@68: jpayne@68: raise Exception('method "%s" is not supported' % method) jpayne@68: jpayne@68: class SimpleXMLRPCRequestHandler(BaseHTTPRequestHandler): jpayne@68: """Simple XML-RPC request handler class. jpayne@68: jpayne@68: Handles all HTTP POST requests and attempts to decode them as jpayne@68: XML-RPC requests. jpayne@68: """ jpayne@68: jpayne@68: # Class attribute listing the accessible path components; jpayne@68: # paths not on this list will result in a 404 error. jpayne@68: rpc_paths = ('/', '/RPC2') jpayne@68: jpayne@68: #if not None, encode responses larger than this, if possible jpayne@68: encode_threshold = 1400 #a common MTU jpayne@68: jpayne@68: #Override form StreamRequestHandler: full buffering of output jpayne@68: #and no Nagle. jpayne@68: wbufsize = -1 jpayne@68: disable_nagle_algorithm = True jpayne@68: jpayne@68: # a re to match a gzip Accept-Encoding jpayne@68: aepattern = re.compile(r""" jpayne@68: \s* ([^\s;]+) \s* #content-coding jpayne@68: (;\s* q \s*=\s* ([0-9\.]+))? #q jpayne@68: """, re.VERBOSE | re.IGNORECASE) jpayne@68: jpayne@68: def accept_encodings(self): jpayne@68: r = {} jpayne@68: ae = self.headers.get("Accept-Encoding", "") jpayne@68: for e in ae.split(","): jpayne@68: match = self.aepattern.match(e) jpayne@68: if match: jpayne@68: v = match.group(3) jpayne@68: v = float(v) if v else 1.0 jpayne@68: r[match.group(1)] = v jpayne@68: return r jpayne@68: jpayne@68: def is_rpc_path_valid(self): jpayne@68: if self.rpc_paths: jpayne@68: return self.path in self.rpc_paths jpayne@68: else: jpayne@68: # If .rpc_paths is empty, just assume all paths are legal jpayne@68: return True jpayne@68: jpayne@68: def do_POST(self): jpayne@68: """Handles the HTTP POST request. jpayne@68: jpayne@68: Attempts to interpret all HTTP POST requests as XML-RPC calls, jpayne@68: which are forwarded to the server's _dispatch method for handling. jpayne@68: """ jpayne@68: jpayne@68: # Check that the path is legal jpayne@68: if not self.is_rpc_path_valid(): jpayne@68: self.report_404() jpayne@68: return jpayne@68: jpayne@68: try: jpayne@68: # Get arguments by reading body of request. jpayne@68: # We read this in chunks to avoid straining jpayne@68: # socket.read(); around the 10 or 15Mb mark, some platforms jpayne@68: # begin to have problems (bug #792570). jpayne@68: max_chunk_size = 10*1024*1024 jpayne@68: size_remaining = int(self.headers["content-length"]) jpayne@68: L = [] jpayne@68: while size_remaining: jpayne@68: chunk_size = min(size_remaining, max_chunk_size) jpayne@68: chunk = self.rfile.read(chunk_size) jpayne@68: if not chunk: jpayne@68: break jpayne@68: L.append(chunk) jpayne@68: size_remaining -= len(L[-1]) jpayne@68: data = b''.join(L) jpayne@68: jpayne@68: data = self.decode_request_content(data) jpayne@68: if data is None: jpayne@68: return #response has been sent jpayne@68: jpayne@68: # In previous versions of SimpleXMLRPCServer, _dispatch jpayne@68: # could be overridden in this class, instead of in jpayne@68: # SimpleXMLRPCDispatcher. To maintain backwards compatibility, jpayne@68: # check to see if a subclass implements _dispatch and dispatch jpayne@68: # using that method if present. jpayne@68: response = self.server._marshaled_dispatch( jpayne@68: data, getattr(self, '_dispatch', None), self.path jpayne@68: ) jpayne@68: except Exception as e: # This should only happen if the module is buggy jpayne@68: # internal error, report as HTTP server error jpayne@68: self.send_response(500) jpayne@68: jpayne@68: # Send information about the exception if requested jpayne@68: if hasattr(self.server, '_send_traceback_header') and \ jpayne@68: self.server._send_traceback_header: jpayne@68: self.send_header("X-exception", str(e)) jpayne@68: trace = traceback.format_exc() jpayne@68: trace = str(trace.encode('ASCII', 'backslashreplace'), 'ASCII') jpayne@68: self.send_header("X-traceback", trace) jpayne@68: jpayne@68: self.send_header("Content-length", "0") jpayne@68: self.end_headers() jpayne@68: else: jpayne@68: self.send_response(200) jpayne@68: self.send_header("Content-type", "text/xml") jpayne@68: if self.encode_threshold is not None: jpayne@68: if len(response) > self.encode_threshold: jpayne@68: q = self.accept_encodings().get("gzip", 0) jpayne@68: if q: jpayne@68: try: jpayne@68: response = gzip_encode(response) jpayne@68: self.send_header("Content-Encoding", "gzip") jpayne@68: except NotImplementedError: jpayne@68: pass jpayne@68: self.send_header("Content-length", str(len(response))) jpayne@68: self.end_headers() jpayne@68: self.wfile.write(response) jpayne@68: jpayne@68: def decode_request_content(self, data): jpayne@68: #support gzip encoding of request jpayne@68: encoding = self.headers.get("content-encoding", "identity").lower() jpayne@68: if encoding == "identity": jpayne@68: return data jpayne@68: if encoding == "gzip": jpayne@68: try: jpayne@68: return gzip_decode(data) jpayne@68: except NotImplementedError: jpayne@68: self.send_response(501, "encoding %r not supported" % encoding) jpayne@68: except ValueError: jpayne@68: self.send_response(400, "error decoding gzip content") jpayne@68: else: jpayne@68: self.send_response(501, "encoding %r not supported" % encoding) jpayne@68: self.send_header("Content-length", "0") jpayne@68: self.end_headers() jpayne@68: jpayne@68: def report_404 (self): jpayne@68: # Report a 404 error jpayne@68: self.send_response(404) jpayne@68: response = b'No such page' jpayne@68: self.send_header("Content-type", "text/plain") jpayne@68: self.send_header("Content-length", str(len(response))) jpayne@68: self.end_headers() jpayne@68: self.wfile.write(response) jpayne@68: jpayne@68: def log_request(self, code='-', size='-'): jpayne@68: """Selectively log an accepted request.""" jpayne@68: jpayne@68: if self.server.logRequests: jpayne@68: BaseHTTPRequestHandler.log_request(self, code, size) jpayne@68: jpayne@68: class SimpleXMLRPCServer(socketserver.TCPServer, jpayne@68: SimpleXMLRPCDispatcher): jpayne@68: """Simple XML-RPC server. jpayne@68: jpayne@68: Simple XML-RPC server that allows functions and a single instance jpayne@68: to be installed to handle requests. The default implementation jpayne@68: attempts to dispatch XML-RPC calls to the functions or instance jpayne@68: installed in the server. Override the _dispatch method inherited jpayne@68: from SimpleXMLRPCDispatcher to change this behavior. jpayne@68: """ jpayne@68: jpayne@68: allow_reuse_address = True jpayne@68: jpayne@68: # Warning: this is for debugging purposes only! Never set this to True in jpayne@68: # production code, as will be sending out sensitive information (exception jpayne@68: # and stack trace details) when exceptions are raised inside jpayne@68: # SimpleXMLRPCRequestHandler.do_POST jpayne@68: _send_traceback_header = False jpayne@68: jpayne@68: def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, jpayne@68: logRequests=True, allow_none=False, encoding=None, jpayne@68: bind_and_activate=True, use_builtin_types=False): jpayne@68: self.logRequests = logRequests jpayne@68: jpayne@68: SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types) jpayne@68: socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate) jpayne@68: jpayne@68: jpayne@68: class MultiPathXMLRPCServer(SimpleXMLRPCServer): jpayne@68: """Multipath XML-RPC Server jpayne@68: This specialization of SimpleXMLRPCServer allows the user to create jpayne@68: multiple Dispatcher instances and assign them to different jpayne@68: HTTP request paths. This makes it possible to run two or more jpayne@68: 'virtual XML-RPC servers' at the same port. jpayne@68: Make sure that the requestHandler accepts the paths in question. jpayne@68: """ jpayne@68: def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, jpayne@68: logRequests=True, allow_none=False, encoding=None, jpayne@68: bind_and_activate=True, use_builtin_types=False): jpayne@68: jpayne@68: SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none, jpayne@68: encoding, bind_and_activate, use_builtin_types) jpayne@68: self.dispatchers = {} jpayne@68: self.allow_none = allow_none jpayne@68: self.encoding = encoding or 'utf-8' jpayne@68: jpayne@68: def add_dispatcher(self, path, dispatcher): jpayne@68: self.dispatchers[path] = dispatcher jpayne@68: return dispatcher jpayne@68: jpayne@68: def get_dispatcher(self, path): jpayne@68: return self.dispatchers[path] jpayne@68: jpayne@68: def _marshaled_dispatch(self, data, dispatch_method = None, path = None): jpayne@68: try: jpayne@68: response = self.dispatchers[path]._marshaled_dispatch( jpayne@68: data, dispatch_method, path) jpayne@68: except: jpayne@68: # report low level exception back to server jpayne@68: # (each dispatcher should have handled their own jpayne@68: # exceptions) jpayne@68: exc_type, exc_value = sys.exc_info()[:2] jpayne@68: try: jpayne@68: response = dumps( jpayne@68: Fault(1, "%s:%s" % (exc_type, exc_value)), jpayne@68: encoding=self.encoding, allow_none=self.allow_none) jpayne@68: response = response.encode(self.encoding, 'xmlcharrefreplace') jpayne@68: finally: jpayne@68: # Break reference cycle jpayne@68: exc_type = exc_value = None jpayne@68: return response jpayne@68: jpayne@68: class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): jpayne@68: """Simple handler for XML-RPC data passed through CGI.""" jpayne@68: jpayne@68: def __init__(self, allow_none=False, encoding=None, use_builtin_types=False): jpayne@68: SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types) jpayne@68: jpayne@68: def handle_xmlrpc(self, request_text): jpayne@68: """Handle a single XML-RPC request""" jpayne@68: jpayne@68: response = self._marshaled_dispatch(request_text) jpayne@68: jpayne@68: print('Content-Type: text/xml') jpayne@68: print('Content-Length: %d' % len(response)) jpayne@68: print() jpayne@68: sys.stdout.flush() jpayne@68: sys.stdout.buffer.write(response) jpayne@68: sys.stdout.buffer.flush() jpayne@68: jpayne@68: def handle_get(self): jpayne@68: """Handle a single HTTP GET request. jpayne@68: jpayne@68: Default implementation indicates an error because jpayne@68: XML-RPC uses the POST method. jpayne@68: """ jpayne@68: jpayne@68: code = 400 jpayne@68: message, explain = BaseHTTPRequestHandler.responses[code] jpayne@68: jpayne@68: response = http.server.DEFAULT_ERROR_MESSAGE % \ jpayne@68: { jpayne@68: 'code' : code, jpayne@68: 'message' : message, jpayne@68: 'explain' : explain jpayne@68: } jpayne@68: response = response.encode('utf-8') jpayne@68: print('Status: %d %s' % (code, message)) jpayne@68: print('Content-Type: %s' % http.server.DEFAULT_ERROR_CONTENT_TYPE) jpayne@68: print('Content-Length: %d' % len(response)) jpayne@68: print() jpayne@68: sys.stdout.flush() jpayne@68: sys.stdout.buffer.write(response) jpayne@68: sys.stdout.buffer.flush() jpayne@68: jpayne@68: def handle_request(self, request_text=None): jpayne@68: """Handle a single XML-RPC request passed through a CGI post method. jpayne@68: jpayne@68: If no XML data is given then it is read from stdin. The resulting jpayne@68: XML-RPC response is printed to stdout along with the correct HTTP jpayne@68: headers. jpayne@68: """ jpayne@68: jpayne@68: if request_text is None and \ jpayne@68: os.environ.get('REQUEST_METHOD', None) == 'GET': jpayne@68: self.handle_get() jpayne@68: else: jpayne@68: # POST data is normally available through stdin jpayne@68: try: jpayne@68: length = int(os.environ.get('CONTENT_LENGTH', None)) jpayne@68: except (ValueError, TypeError): jpayne@68: length = -1 jpayne@68: if request_text is None: jpayne@68: request_text = sys.stdin.read(length) jpayne@68: jpayne@68: self.handle_xmlrpc(request_text) jpayne@68: jpayne@68: jpayne@68: # ----------------------------------------------------------------------------- jpayne@68: # Self documenting XML-RPC Server. jpayne@68: jpayne@68: class ServerHTMLDoc(pydoc.HTMLDoc): jpayne@68: """Class used to generate pydoc HTML document for a server""" jpayne@68: jpayne@68: def markup(self, text, escape=None, funcs={}, classes={}, methods={}): jpayne@68: """Mark up some plain text, given a context of symbols to look for. jpayne@68: Each context dictionary maps object names to anchor names.""" jpayne@68: escape = escape or self.escape jpayne@68: results = [] jpayne@68: here = 0 jpayne@68: jpayne@68: # XXX Note that this regular expression does not allow for the jpayne@68: # hyperlinking of arbitrary strings being used as method jpayne@68: # names. Only methods with names consisting of word characters jpayne@68: # and '.'s are hyperlinked. jpayne@68: pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|' jpayne@68: r'RFC[- ]?(\d+)|' jpayne@68: r'PEP[- ]?(\d+)|' jpayne@68: r'(self\.)?((?:\w|\.)+))\b') jpayne@68: while 1: jpayne@68: match = pattern.search(text, here) jpayne@68: if not match: break jpayne@68: start, end = match.span() jpayne@68: results.append(escape(text[here:start])) jpayne@68: jpayne@68: all, scheme, rfc, pep, selfdot, name = match.groups() jpayne@68: if scheme: jpayne@68: url = escape(all).replace('"', '"') jpayne@68: results.append('%s' % (url, url)) jpayne@68: elif rfc: jpayne@68: url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc) jpayne@68: results.append('%s' % (url, escape(all))) jpayne@68: elif pep: jpayne@68: url = 'http://www.python.org/dev/peps/pep-%04d/' % int(pep) jpayne@68: results.append('%s' % (url, escape(all))) jpayne@68: elif text[end:end+1] == '(': jpayne@68: results.append(self.namelink(name, methods, funcs, classes)) jpayne@68: elif selfdot: jpayne@68: results.append('self.%s' % name) jpayne@68: else: jpayne@68: results.append(self.namelink(name, classes)) jpayne@68: here = end jpayne@68: results.append(escape(text[here:])) jpayne@68: return ''.join(results) jpayne@68: jpayne@68: def docroutine(self, object, name, mod=None, jpayne@68: funcs={}, classes={}, methods={}, cl=None): jpayne@68: """Produce HTML documentation for a function or method object.""" jpayne@68: jpayne@68: anchor = (cl and cl.__name__ or '') + '-' + name jpayne@68: note = '' jpayne@68: jpayne@68: title = '%s' % ( jpayne@68: self.escape(anchor), self.escape(name)) jpayne@68: jpayne@68: if callable(object): jpayne@68: argspec = str(signature(object)) jpayne@68: else: jpayne@68: argspec = '(...)' jpayne@68: jpayne@68: if isinstance(object, tuple): jpayne@68: argspec = object[0] or argspec jpayne@68: docstring = object[1] or "" jpayne@68: else: jpayne@68: docstring = pydoc.getdoc(object) jpayne@68: jpayne@68: decl = title + argspec + (note and self.grey( jpayne@68: '%s' % note)) jpayne@68: jpayne@68: doc = self.markup( jpayne@68: docstring, self.preformat, funcs, classes, methods) jpayne@68: doc = doc and '
%s
\n' % doc jpayne@68: jpayne@68: contents = [] jpayne@68: method_items = sorted(methods.items()) jpayne@68: for key, value in method_items: jpayne@68: contents.append(self.docroutine(value, key, funcs=fdict)) jpayne@68: result = result + self.bigsection( jpayne@68: 'Methods', '#ffffff', '#eeaa77', ''.join(contents)) jpayne@68: jpayne@68: return result jpayne@68: jpayne@68: class XMLRPCDocGenerator: jpayne@68: """Generates documentation for an XML-RPC server. jpayne@68: jpayne@68: This class is designed as mix-in and should not jpayne@68: be constructed directly. jpayne@68: """ jpayne@68: jpayne@68: def __init__(self): jpayne@68: # setup variables used for HTML documentation jpayne@68: self.server_name = 'XML-RPC Server Documentation' jpayne@68: self.server_documentation = \ jpayne@68: "This server exports the following methods through the XML-RPC "\ jpayne@68: "protocol." jpayne@68: self.server_title = 'XML-RPC Server Documentation' jpayne@68: jpayne@68: def set_server_title(self, server_title): jpayne@68: """Set the HTML title of the generated server documentation""" jpayne@68: jpayne@68: self.server_title = server_title jpayne@68: jpayne@68: def set_server_name(self, server_name): jpayne@68: """Set the name of the generated HTML server documentation""" jpayne@68: jpayne@68: self.server_name = server_name jpayne@68: jpayne@68: def set_server_documentation(self, server_documentation): jpayne@68: """Set the documentation string for the entire server.""" jpayne@68: jpayne@68: self.server_documentation = server_documentation jpayne@68: jpayne@68: def generate_html_documentation(self): jpayne@68: """generate_html_documentation() => html documentation for the server jpayne@68: jpayne@68: Generates HTML documentation for the server using introspection for jpayne@68: installed functions and instances that do not implement the jpayne@68: _dispatch method. Alternatively, instances can choose to implement jpayne@68: the _get_method_argstring(method_name) method to provide the jpayne@68: argument string used in the documentation and the jpayne@68: _methodHelp(method_name) method to provide the help text used jpayne@68: in the documentation.""" jpayne@68: jpayne@68: methods = {} jpayne@68: jpayne@68: for method_name in self.system_listMethods(): jpayne@68: if method_name in self.funcs: jpayne@68: method = self.funcs[method_name] jpayne@68: elif self.instance is not None: jpayne@68: method_info = [None, None] # argspec, documentation jpayne@68: if hasattr(self.instance, '_get_method_argstring'): jpayne@68: method_info[0] = self.instance._get_method_argstring(method_name) jpayne@68: if hasattr(self.instance, '_methodHelp'): jpayne@68: method_info[1] = self.instance._methodHelp(method_name) jpayne@68: jpayne@68: method_info = tuple(method_info) jpayne@68: if method_info != (None, None): jpayne@68: method = method_info jpayne@68: elif not hasattr(self.instance, '_dispatch'): jpayne@68: try: jpayne@68: method = resolve_dotted_attribute( jpayne@68: self.instance, jpayne@68: method_name jpayne@68: ) jpayne@68: except AttributeError: jpayne@68: method = method_info jpayne@68: else: jpayne@68: method = method_info jpayne@68: else: jpayne@68: assert 0, "Could not find method in self.functions and no "\ jpayne@68: "instance installed" jpayne@68: jpayne@68: methods[method_name] = method jpayne@68: jpayne@68: documenter = ServerHTMLDoc() jpayne@68: documentation = documenter.docserver( jpayne@68: self.server_name, jpayne@68: self.server_documentation, jpayne@68: methods jpayne@68: ) jpayne@68: jpayne@68: return documenter.page(html.escape(self.server_title), documentation) jpayne@68: jpayne@68: class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): jpayne@68: """XML-RPC and documentation request handler class. jpayne@68: jpayne@68: Handles all HTTP POST requests and attempts to decode them as jpayne@68: XML-RPC requests. jpayne@68: jpayne@68: Handles all HTTP GET requests and interprets them as requests jpayne@68: for documentation. jpayne@68: """ jpayne@68: jpayne@68: def do_GET(self): jpayne@68: """Handles the HTTP GET request. jpayne@68: jpayne@68: Interpret all HTTP GET requests as requests for server jpayne@68: documentation. jpayne@68: """ jpayne@68: # Check that the path is legal jpayne@68: if not self.is_rpc_path_valid(): jpayne@68: self.report_404() jpayne@68: return jpayne@68: jpayne@68: response = self.server.generate_html_documentation().encode('utf-8') jpayne@68: self.send_response(200) jpayne@68: self.send_header("Content-type", "text/html") jpayne@68: self.send_header("Content-length", str(len(response))) jpayne@68: self.end_headers() jpayne@68: self.wfile.write(response) jpayne@68: jpayne@68: class DocXMLRPCServer( SimpleXMLRPCServer, jpayne@68: XMLRPCDocGenerator): jpayne@68: """XML-RPC and HTML documentation server. jpayne@68: jpayne@68: Adds the ability to serve server documentation to the capabilities jpayne@68: of SimpleXMLRPCServer. jpayne@68: """ jpayne@68: jpayne@68: def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler, jpayne@68: logRequests=True, allow_none=False, encoding=None, jpayne@68: bind_and_activate=True, use_builtin_types=False): jpayne@68: SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, jpayne@68: allow_none, encoding, bind_and_activate, jpayne@68: use_builtin_types) jpayne@68: XMLRPCDocGenerator.__init__(self) jpayne@68: jpayne@68: class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler, jpayne@68: XMLRPCDocGenerator): jpayne@68: """Handler for XML-RPC data and documentation requests passed through jpayne@68: CGI""" jpayne@68: jpayne@68: def handle_get(self): jpayne@68: """Handles the HTTP GET request. jpayne@68: jpayne@68: Interpret all HTTP GET requests as requests for server jpayne@68: documentation. jpayne@68: """ jpayne@68: jpayne@68: response = self.generate_html_documentation().encode('utf-8') jpayne@68: jpayne@68: print('Content-Type: text/html') jpayne@68: print('Content-Length: %d' % len(response)) jpayne@68: print() jpayne@68: sys.stdout.flush() jpayne@68: sys.stdout.buffer.write(response) jpayne@68: sys.stdout.buffer.flush() jpayne@68: jpayne@68: def __init__(self): jpayne@68: CGIXMLRPCRequestHandler.__init__(self) jpayne@68: XMLRPCDocGenerator.__init__(self) jpayne@68: jpayne@68: jpayne@68: if __name__ == '__main__': jpayne@68: import datetime jpayne@68: jpayne@68: class ExampleService: jpayne@68: def getData(self): jpayne@68: return '42' jpayne@68: jpayne@68: class currentTime: jpayne@68: @staticmethod jpayne@68: def getCurrentTime(): jpayne@68: return datetime.datetime.now() jpayne@68: jpayne@68: with SimpleXMLRPCServer(("localhost", 8000)) as server: jpayne@68: server.register_function(pow) jpayne@68: server.register_function(lambda x,y: x+y, 'add') jpayne@68: server.register_instance(ExampleService(), allow_dotted_names=True) jpayne@68: server.register_multicall_functions() jpayne@68: print('Serving XML-RPC on localhost port 8000') jpayne@68: print('It is advisable to run this example server within a secure, closed network.') jpayne@68: try: jpayne@68: server.serve_forever() jpayne@68: except KeyboardInterrupt: jpayne@68: print("\nKeyboard interrupt received, exiting.") jpayne@68: sys.exit(0)