From d69663d3009b6718ed3fa27dec800d15c7d4babb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20v=2E=20L=C3=B6wis?= Date: Wed, 15 Jan 2003 11:37:23 +0000 Subject: Patch #473586: Implement CGIXMLRPCRequestHandler. --- Doc/lib/libsimplexmlrpc.tex | 119 +++++++++-- Lib/SimpleXMLRPCServer.py | 480 ++++++++++++++++++++++++++++++++++---------- Misc/NEWS | 3 + 3 files changed, 477 insertions(+), 125 deletions(-) diff --git a/Doc/lib/libsimplexmlrpc.tex b/Doc/lib/libsimplexmlrpc.tex index be56c45..6531b52 100644 --- a/Doc/lib/libsimplexmlrpc.tex +++ b/Doc/lib/libsimplexmlrpc.tex @@ -8,14 +8,13 @@ The \module{SimpleXMLRPCServer} module provides a basic server -framework for XML-RPC servers written in Python. The server object is -based on the \class{\refmodule{SocketServer}.TCPServer} class, -and the request handler is based on the -\class{\refmodule{BaseHTTPServer}.BaseHTTPRequestHandler} class. - +framework for XML-RPC servers written in Python. Servers can either +be free standing, using \class{SimpleXMLRPCServer}, or embedded in a +CGI environment, using \class{CGIXMLRPCRequestHandler}. \begin{classdesc}{SimpleXMLRPCServer}{addr\optional{, requestHandler\optional{, logRequests}}} + Create a new server instance. The \var{requestHandler} parameter should be a factory for request handler instances; it defaults to \class{SimpleXMLRPCRequestHandler}. The \var{addr} and @@ -27,6 +26,10 @@ and the request handler is based on the the XML-RPC protocol. \end{classdesc} +\begin{classdesc}{CGIXMLRPCRequestHandler}{} + Create a new instance to handle XML-RPC requests in a CGI + environment. \versionadded{2.3} +\end{classdesc} \begin{classdesc}{SimpleXMLRPCRequestHandler}{} Create a new request handler instance. This request handler @@ -38,20 +41,18 @@ and the request handler is based on the \subsection{SimpleXMLRPCServer Objects \label{simple-xmlrpc-servers}} -The \class{SimpleXMLRPCServer} class provides two methods that an -application can use to register functions that can be called via the -XML-RPC protocol via the request handler. +The \class{SimpleXMLRPCServer} class is based on +\class{SocketServer.TCPServer} and provides a means of creating +simple, stand alone XML-RPC servers. \begin{methoddesc}[SimpleXMLRPCServer]{register_function}{function\optional{, name}} - Register a function that can respond to XML-RPC requests. The - function must be callable with a single parameter which will be the - return value of \function{\refmodule{xmlrpclib}.loads()} when called - with the payload of the request. If \var{name} is given, it will be - the method name associated with \var{function}, otherwise - \code{\var{function}.__name__} will be used. \var{name} can be - either a normal or Unicode string, and may contain characters not - legal in Python identifiers, including the period character. + Register a function that can respond to XML-RPC requests. If + \var{name} is given, it will be the method name associated with + \var{function}, otherwise \code{\var{function}.__name__} will be + used. \var{name} can be either a normal or Unicode string, and may + contain characters not legal in Python identifiers, including the + period character. \end{methoddesc} \begin{methoddesc}[SimpleXMLRPCServer]{register_instance}{instance} @@ -68,3 +69,89 @@ XML-RPC protocol via the request handler. search is then called with the parameters from the request, and the return value is passed back to the client. \end{methoddesc} + +\begin{methoddesc}{register_introspection_functions}{} + Registers the XML-RPC introspection functions \code{system.listMethods}, + \code{system.methodHelp} and \code{system.methodSignature}. + \versionadded{2.3} +\end{methoddesc} + +\begin{methoddesc}{register_multicall_functions}{} + Registers the XML-RPC multicall function system.multicall. +\end{methoddesc} + +Example: + +\begin{verbatim} +class MyFuncs: + def div(self, x, y) : return div(x,y) + + +server = SimpleXMLRPCServer(("localhost", 8000)) +server.register_function(pow) +server.register_function(lambda x,y: x+y, 'add') +server.register_introspection_functions() +server.register_instance(MyFuncs()) +server.serve_forever() +\end{verbatim} + +\subsection{CGIXMLRPCRequestHandler} + +The \class{CGIXMLRPCRequestHandler} class can be used to +handle XML-RPC requests sent to Python CGI scripts. + +\begin{methoddesc}{register_function}{function\optional{, name}} +Register a function that can respond to XML-RPC requests. If +\var{name] is given, it will be the method name associated with +function, otherwise \var{function.__name__} will be used. \var{name} +can be either a normal or Unicode string, and may contain +characters not legal in Python identifiers, including the period +character. +\end{methoddesc} + +\begin{methoddesc}{register_instance}{instance} +Register an object which is used to expose method names +which have not been registered using \method{register_function()}. If +instance contains a \method{_dispatch()} method, it is called with the +requested method name and the parameters from the +request; the return value is returned to the client as the result. +If instance does not have a \methode{_dispatch()} method, it is searched +for an attribute matching the name of the requested method; if +the requested method name contains periods, each +component of the method name is searched for individually, +with the effect that a simple hierarchical search is performed. +The value found from this search is then called with the +parameters from the request, and the return value is passed +back to the client. +\end{methoddesc} + +\begin{methoddesc}{register_introspection_functions}{} +Register the XML-RPC introspection functions +\code{system.listMethods}, \code{system.methodHelp} and +\code{system.methodSignature}. +\end{methoddesc} + +\begin{methoddesc}{register_multicall_functions}{} +Register the XML-RPC multicall function \code{system.multicall}. +\end{methoddesc} + +\begin{methoddesc}{handle_request}{\optional{request_text = None}} +Handle a XML-RPC request. If \var{request_text} is given, it +should be the POST data provided by the HTTP server, +otherwise the contents of stdin will be used. +\end{methoddesc} + +Example: + +\begin{verbatim} +class MyFuncs: + def div(self, x, y) : return div(x,y) + + +handler = CGIXMLRPCRequestHandler() +handler.register_function(pow) +handler.register_function(lambda x,y: x+y, 'add') +handler.register_introspection_functions() +handler.register_instance(MyFuncs()) +handler.handle_request() +\end{verbatim} \ No newline at end of file diff --git a/Lib/SimpleXMLRPCServer.py b/Lib/SimpleXMLRPCServer.py index 0a91683..6320184 100644 --- a/Lib/SimpleXMLRPCServer.py +++ b/Lib/SimpleXMLRPCServer.py @@ -2,9 +2,12 @@ This module can be used to create simple XML-RPC servers by creating a server and either installing functions, a -class instance, or by extending the SimpleXMLRPCRequestHandler +class instance, or by extending the SimpleXMLRPCServer class. +It can also be used to handle XML-RPC requests in a CGI +environment using CGIXMLRPCRequestHandler. + A list of possible usage patterns follows: 1. Install functions: @@ -22,29 +25,53 @@ class MyFuncs: # string.func_name import string self.string = string + def _listMethods(self): + # implement this method so that system.listMethods + # knows to advertise the strings methods + return list_public_methods(self) + \ + ['string.' + method for method in list_public_methods(self.string)] def pow(self, x, y): return pow(x, y) def add(self, x, y) : return x + y + server = SimpleXMLRPCServer(("localhost", 8000)) +server.register_introspection_functions() server.register_instance(MyFuncs()) server.serve_forever() 3. Install an instance with custom dispatch method: class Math: + def _listMethods(self): + # this method must be present for system.listMethods + # to work + return ['add', 'pow'] + def _methodHelp(self, method): + # this method must be present for system.methodHelp + # to work + if method == 'add': + return "add(2,3) => 5" + elif method == 'pow': + return "pow(x, y[, z]) => number" + else: + # By convention, return empty + # string if no help is available + return "" def _dispatch(self, method, params): if method == 'pow': - return apply(pow, params) + return pow(*params) elif method == 'add': return params[0] + params[1] else: raise 'bad method' + server = SimpleXMLRPCServer(("localhost", 8000)) +server.register_introspection_functions() server.register_instance(Math()) server.serve_forever() -4. Subclass SimpleXMLRPCRequestHandler: +4. Subclass SimpleXMLRPCServer: -class MathHandler(SimpleXMLRPCRequestHandler): +class MathServer(SimpleXMLRPCServer): def _dispatch(self, method, params): try: # We are forcing the 'export_' prefix on methods that are @@ -54,78 +81,263 @@ class MathHandler(SimpleXMLRPCRequestHandler): except AttributeError: raise Exception('method "%s" is not supported' % method) else: - return apply(func, params) - - def log_message(self, format, *args): - pass # maybe do something fancy like write the messages to a file + return func(*params) def export_add(self, x, y): return x + y -server = SimpleXMLRPCServer(("localhost", 8000), MathHandler) +server = MathServer(("localhost", 8000)) server.serve_forever() + +5. CGI script: + +server = CGIXMLRPCRequestHandler() +server.register_function(pow) +server.handle_request() """ # Written by Brian Quinlan (brian@sweetapp.com). # Based on code written by Fredrik Lundh. import xmlrpclib +from xmlrpclib import Fault import SocketServer import BaseHTTPServer import sys +import types +import os -class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """Simple XML-RPC request handler class. +def resolve_dotted_attribute(obj, attr): + """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d - Handles all HTTP POST requests and attempts to decode them as - XML-RPC requests. + Resolves a dotted attribute name to an object. Raises + an AttributeError if any attribute in the chain starts with a '_'. + """ + + for i in attr.split('.'): + if i.startswith('_'): + raise AttributeError( + 'attempt to access private attribute "%s"' % i + ) + else: + obj = getattr(obj,i) + return obj - XML-RPC requests are dispatched to the _dispatch method, which - may be overriden by subclasses. The default implementation attempts - to dispatch XML-RPC calls to the functions or instance installed - in the server. +def list_public_methods(obj): + """Returns a list of attribute strings, found in the specified + object, which represent callable attributes""" + + return [member for member in dir(obj) + if not member.startswith('_') and + callable(getattr(obj, member))] + +def remove_duplicates(lst): + """remove_duplicates([2,2,2,1,3,3]) => [3,1,2] + + Returns a copy of a list without duplicates. Every list + item must be hashable and the order of the items in the + resulting list is not defined. + """ + u = {} + for x in lst: + u[x] = 1 + + return u.keys() + +class SimpleXMLRPCDispatcher: + """Mix-in class that dispatches XML-RPC requests. + + This class is used to register XML-RPC method handlers + and then to dispatch them. There should never be any + reason to instantiate this class directly. """ + + def __init__(self): + self.funcs = {} + self.instance = None - def do_POST(self): - """Handles the HTTP POST request. + def register_instance(self, instance): + """Registers an instance to respond to XML-RPC requests. - Attempts to interpret all HTTP POST requests as XML-RPC calls, - which are forwarded to the _dispatch method for handling. + Only one instance can be installed at a time. + + If the registered instance has a _dispatch method then that + method will be called with the name of the XML-RPC method and + it's parameters as a tuple + e.g. instance._dispatch('add',(2,3)) + + If the registered instance does not have a _dispatch method + then the instance will be searched to find a matching method + and, if found, will be called. Methods beginning with an '_' + are considered private and will not be called by + SimpleXMLRPCServer. + + If a registered function matches a XML-RPC request, then it + will be called instead of the registered instance. """ - try: - # get arguments - data = self.rfile.read(int(self.headers["content-length"])) - params, method = xmlrpclib.loads(data) + self.instance = instance - # generate response - try: + def register_function(self, function, name = None): + """Registers a function to respond to XML-RPC requests. + + The optional name argument can be used to set a Unicode name + for the function. + """ + + if name is None: + name = function.__name__ + self.funcs[name] = function + + def register_introspection_functions(self): + """Registers the XML-RPC introspection methods in the system + namespace. + + see http://xmlrpc.usefulinc.com/doc/reserved.html + """ + + self.funcs.update({'system.listMethods' : self.system_listMethods, + 'system.methodSignature' : self.system_methodSignature, + 'system.methodHelp' : self.system_methodHelp}) + + def register_multicall_functions(self): + """Registers the XML-RPC multicall method in the system + namespace. + + see http://www.xmlrpc.com/discuss/msgReader$1208""" + + self.funcs.update({'system.multicall' : self.system_multicall}) + + def _marshaled_dispatch(self, data, dispatch_method = None): + """Dispatches an XML-RPC method from marshalled (XML) data. + + XML-RPC methods are dispatched from the marshalled (XML) data + using the _dispatch method and the result is returned as + marshalled data. For backwards compatibility, a dispatch + function can be provided as an argument (see comment in + SimpleXMLRPCRequestHandler.do_POST) but overriding the + existing method through subclassing is the prefered means + of changing method dispatch behavior. + """ + + params, method = xmlrpclib.loads(data) + + # generate response + try: + if dispatch_method is not None: + response = dispatch_method(method, params) + else: response = self._dispatch(method, params) - # wrap response in a singleton tuple - response = (response,) - except: - # report exception back to server - response = xmlrpclib.dumps( - xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)) - ) - else: - response = xmlrpclib.dumps(response, methodresponse=1) + # wrap response in a singleton tuple + response = (response,) + response = xmlrpclib.dumps(response, methodresponse=1) + except Fault, fault: + response = xmlrpclib.dumps(fault) except: - # internal error, report as HTTP server error - self.send_response(500) - self.end_headers() + # report exception back to server + response = xmlrpclib.dumps( + xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)) + ) + + return response + + def system_listMethods(self): + """system.listMethods() => ['add', 'subtract', 'multiple'] + + Returns a list of the methods supported by the server.""" + + methods = self.funcs.keys() + if self.instance is not None: + # Instance can implement _listMethod to return a list of + # methods + if hasattr(self.instance, '_listMethods'): + methods = remove_duplicates( + methods + self.instance._listMethods() + ) + # if the instance has a _dispatch method then we + # don't have enough information to provide a list + # of methods + elif not hasattr(self.instance, '_dispatch'): + methods = remove_duplicates( + methods + list_public_methods(self.instance) + ) + methods.sort() + return methods + + def system_methodSignature(self, method_name): + """system.methodSignature('add') => [double, int, int] + + Returns a list describing the signiture of the method. In the + above example, the add method takes two integers as arguments + and returns a double result. + + This server does NOT support system.methodSignature.""" + + # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html + + return 'signatures not supported' + + def system_methodHelp(self, method_name): + """system.methodHelp('add') => "Adds two integers together" + + Returns a string containing documentation for the specified method.""" + + method = None + if self.funcs.has_key(method_name): + method = self.funcs[method_name] + elif self.instance is not None: + # Instance can implement _methodHelp to return help for a method + if hasattr(self.instance, '_methodHelp'): + return self.instance._methodHelp(method_name) + # if the instance has a _dispatch method then we + # don't have enough information to provide help + elif not hasattr(self.instance, '_dispatch'): + try: + method = resolve_dotted_attribute( + self.instance, + method_name + ) + except AttributeError: + pass + + # Note that we aren't checking that the method actually + # be a callable object of some kind + if method is None: + return "" else: - # got a valid XML RPC response - self.send_response(200) - self.send_header("Content-type", "text/xml") - self.send_header("Content-length", str(len(response))) - self.end_headers() - self.wfile.write(response) + return pydoc.getdoc(method) - # shut down the connection - self.wfile.flush() - self.connection.shutdown(1) + def system_multicall(self, call_list): + """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \ +[[4], ...] + + Allows the caller to package multiple XML-RPC calls into a single + request. + See http://www.xmlrpc.com/discuss/msgReader$1208 + """ + + results = [] + for call in call_list: + method_name = call['methodName'] + params = call['params'] + + try: + # XXX A marshalling error in any response will fail the entire + # multicall. If someone cares they should fix this. + results.append([self._dispatch(method_name, params)]) + except Fault, fault: + results.append( + {'faultCode' : fault.faultCode, + 'faultString' : fault.faultString} + ) + except: + results.append( + {'faultCode' : 1, + 'faultString' : "%s:%s" % (sys.exc_type, sys.exc_value)} + ) + return results + def _dispatch(self, method, params): """Dispatches the XML-RPC method. @@ -144,105 +356,155 @@ class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): and, if found, will be called. Methods beginning with an '_' are considered private and will - not be called by SimpleXMLRPCServer. + not be called. """ func = None try: # check to see if a matching function has been registered - func = self.server.funcs[method] + func = self.funcs[method] except KeyError: - if self.server.instance is not None: + if self.instance is not None: # check for a _dispatch method - if hasattr(self.server.instance, '_dispatch'): - return self.server.instance._dispatch(method, params) + if hasattr(self.instance, '_dispatch'): + return self.instance._dispatch(method, params) else: # call instance method directly try: - func = _resolve_dotted_attribute( - self.server.instance, + func = resolve_dotted_attribute( + self.instance, method ) except AttributeError: pass if func is not None: - return apply(func, params) + return func(*params) else: raise Exception('method "%s" is not supported' % method) + +class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """Simple XML-RPC request handler class. - def log_request(self, code='-', size='-'): - """Selectively log an accepted request.""" - - if self.server.logRequests: - BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size) + Handles all HTTP POST requests and attempts to decode them as + XML-RPC requests. + """ + def do_POST(self): + """Handles the HTTP POST request. -def _resolve_dotted_attribute(obj, attr): - """Resolves a dotted attribute name to an object. Raises - an AttributeError if any attribute in the chain starts with a '_'. - """ - for i in attr.split('.'): - if i.startswith('_'): - raise AttributeError( - 'attempt to access private attribute "%s"' % i + Attempts to interpret all HTTP POST requests as XML-RPC calls, + which are forwarded to the server's _dispatch method for handling. + """ + + try: + # get arguments + data = self.rfile.read(int(self.headers["content-length"])) + # In previous versions of SimpleXMLRPCServer, _dispatch + # could be overridden in this class, instead of in + # SimpleXMLRPCDispatcher. To maintain backwards compatibility, + # check to see if a subclass implements _dispatch and dispatch + # using that method if present. + response = self.server._marshaled_dispatch( + data, getattr(self, '_dispatch', None) ) + except: # This should only happen if the module is buggy + # internal error, report as HTTP server error + self.send_response(500) + self.end_headers() else: - obj = getattr(obj,i) - return obj + # got a valid XML RPC response + self.send_response(200) + self.send_header("Content-type", "text/xml") + self.send_header("Content-length", str(len(response))) + self.end_headers() + self.wfile.write(response) + # shut down the connection + self.wfile.flush() + self.connection.shutdown(1) + + def log_request(self, code='-', size='-'): + """Selectively log an accepted request.""" -class SimpleXMLRPCServer(SocketServer.TCPServer): + if self.server.logRequests: + BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size) + +class SimpleXMLRPCServer(SocketServer.TCPServer, + SimpleXMLRPCDispatcher): """Simple XML-RPC server. Simple XML-RPC server that allows functions and a single instance - to be installed to handle requests. + to be installed to handle requests. The default implementation + attempts to dispatch XML-RPC calls to the functions or instance + installed in the server. Override the _dispatch method inhereted + from SimpleXMLRPCDispatcher to change this behavior. """ def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, logRequests=1): - self.funcs = {} self.logRequests = logRequests - self.instance = None + + SimpleXMLRPCDispatcher.__init__(self) SocketServer.TCPServer.__init__(self, addr, requestHandler) - - def register_instance(self, instance): - """Registers an instance to respond to XML-RPC requests. - - Only one instance can be installed at a time. - - If the registered instance has a _dispatch method then that - method will be called with the name of the XML-RPC method and - it's parameters as a tuple - e.g. instance._dispatch('add',(2,3)) - - If the registered instance does not have a _dispatch method - then the instance will be searched to find a matching method - and, if found, will be called. - - Methods beginning with an '_' are considered private and will - not be called by SimpleXMLRPCServer. - - If a registered function matches a XML-RPC request, then it - will be called instead of the registered instance. + +class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): + """Simple handler for XML-RPC data passed through CGI.""" + + def __init__(self): + SimpleXMLRPCDispatcher.__init__(self) + + def handle_xmlrpc(self, request_text): + """Handle a single XML-RPC request""" + + response = self._marshaled_dispatch(request_text) + + print 'Content-Type: text/xml' + print 'Content-Length: %d' % len(response) + print + print response + + def handle_get(self): + """Handle a single HTTP GET request. + + Default implementation indicates an error because + XML-RPC uses the POST method. """ - self.instance = instance - - def register_function(self, function, name = None): - """Registers a function to respond to XML-RPC requests. - - The optional name argument can be used to set a Unicode name - for the function. - - If an instance is also registered then it will only be called - if a matching function is not found. + code = 400 + message, explain = \ + BaseHTTPServer.BaseHTTPRequestHandler.responses[code] + + response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \ + { + 'code' : code, + 'message' : message, + 'explain' : explain + } + print 'Status: %d %s' % (code, message) + print 'Content-Type: text/html' + print 'Content-Length: %d' % len(response) + print + print response + + def handle_request(self, request_text = None): + """Handle a single XML-RPC request passed through a CGI post method. + + If no XML data is given then it is read from stdin. The resulting + XML-RPC response is printed to stdout along with the correct HTTP + headers. """ + + if request_text is None and \ + os.environ.get('REQUEST_METHOD', None) == 'GET': + self.handle_get() + else: + # POST data is normally available through stdin + if request_text is None: + request_text = sys.stdin.read() - if name is None: - name = function.__name__ - self.funcs[name] = function - + self.handle_xmlrpc(request_text) + if __name__ == '__main__': server = SimpleXMLRPCServer(("localhost", 8000)) server.register_function(pow) diff --git a/Misc/NEWS b/Misc/NEWS index 29411ec..61caf76 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -78,6 +78,9 @@ Extension modules Library ------- +- SimpleXMLRPCServer now supports CGI through the CGIXMLRPCRequestHandler + class. + - The sets module now raises TypeError in __cmp__, to clarify that sets are not intended to be three-way-compared; the comparison operators are overloaded as subset/superset tests. -- cgit v0.12