Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | """Simple XML-RPC Server. |
2 | ||
3 | This module can be used to create simple XML-RPC servers | |
4 | by creating a server and either installing functions, a | |
5 | class instance, or by extending the SimpleXMLRPCServer | |
6 | class. | |
7 | ||
8 | It can also be used to handle XML-RPC requests in a CGI | |
9 | environment using CGIXMLRPCRequestHandler. | |
10 | ||
11 | A list of possible usage patterns follows: | |
12 | ||
13 | 1. Install functions: | |
14 | ||
15 | server = SimpleXMLRPCServer(("localhost", 8000)) | |
16 | server.register_function(pow) | |
17 | server.register_function(lambda x,y: x+y, 'add') | |
18 | server.serve_forever() | |
19 | ||
20 | 2. Install an instance: | |
21 | ||
22 | class MyFuncs: | |
23 | def __init__(self): | |
24 | # make all of the string functions available through | |
25 | # string.func_name | |
26 | import string | |
27 | self.string = string | |
28 | def _listMethods(self): | |
29 | # implement this method so that system.listMethods | |
30 | # knows to advertise the strings methods | |
31 | return list_public_methods(self) + \ | |
32 | ['string.' + method for method in list_public_methods(self.string)] | |
33 | def pow(self, x, y): return pow(x, y) | |
34 | def add(self, x, y) : return x + y | |
35 | ||
36 | server = SimpleXMLRPCServer(("localhost", 8000)) | |
37 | server.register_introspection_functions() | |
38 | server.register_instance(MyFuncs()) | |
39 | server.serve_forever() | |
40 | ||
41 | 3. Install an instance with custom dispatch method: | |
42 | ||
43 | class Math: | |
44 | def _listMethods(self): | |
45 | # this method must be present for system.listMethods | |
46 | # to work | |
47 | return ['add', 'pow'] | |
48 | def _methodHelp(self, method): | |
49 | # this method must be present for system.methodHelp | |
50 | # to work | |
51 | if method == 'add': | |
52 | return "add(2,3) => 5" | |
53 | elif method == 'pow': | |
54 | return "pow(x, y[, z]) => number" | |
55 | else: | |
56 | # By convention, return empty | |
57 | # string if no help is available | |
58 | return "" | |
59 | def _dispatch(self, method, params): | |
60 | if method == 'pow': | |
61 | return pow(*params) | |
62 | elif method == 'add': | |
63 | return params[0] + params[1] | |
64 | else: | |
65 | raise 'bad method' | |
66 | ||
67 | server = SimpleXMLRPCServer(("localhost", 8000)) | |
68 | server.register_introspection_functions() | |
69 | server.register_instance(Math()) | |
70 | server.serve_forever() | |
71 | ||
72 | 4. Subclass SimpleXMLRPCServer: | |
73 | ||
74 | class MathServer(SimpleXMLRPCServer): | |
75 | def _dispatch(self, method, params): | |
76 | try: | |
77 | # We are forcing the 'export_' prefix on methods that are | |
78 | # callable through XML-RPC to prevent potential security | |
79 | # problems | |
80 | func = getattr(self, 'export_' + method) | |
81 | except AttributeError: | |
82 | raise Exception('method "%s" is not supported' % method) | |
83 | else: | |
84 | return func(*params) | |
85 | ||
86 | def export_add(self, x, y): | |
87 | return x + y | |
88 | ||
89 | server = MathServer(("localhost", 8000)) | |
90 | server.serve_forever() | |
91 | ||
92 | 5. CGI script: | |
93 | ||
94 | server = CGIXMLRPCRequestHandler() | |
95 | server.register_function(pow) | |
96 | server.handle_request() | |
97 | """ | |
98 | ||
99 | # Written by Brian Quinlan (brian@sweetapp.com). | |
100 | # Based on code written by Fredrik Lundh. | |
101 | ||
102 | import xmlrpclib | |
103 | from xmlrpclib import Fault | |
104 | import SocketServer | |
105 | import BaseHTTPServer | |
106 | import sys | |
107 | import os | |
108 | ||
109 | def resolve_dotted_attribute(obj, attr, allow_dotted_names=True): | |
110 | """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d | |
111 | ||
112 | Resolves a dotted attribute name to an object. Raises | |
113 | an AttributeError if any attribute in the chain starts with a '_'. | |
114 | ||
115 | If the optional allow_dotted_names argument is false, dots are not | |
116 | supported and this function operates similar to getattr(obj, attr). | |
117 | """ | |
118 | ||
119 | if allow_dotted_names: | |
120 | attrs = attr.split('.') | |
121 | else: | |
122 | attrs = [attr] | |
123 | ||
124 | for i in attrs: | |
125 | if i.startswith('_'): | |
126 | raise AttributeError( | |
127 | 'attempt to access private attribute "%s"' % i | |
128 | ) | |
129 | else: | |
130 | obj = getattr(obj,i) | |
131 | return obj | |
132 | ||
133 | def list_public_methods(obj): | |
134 | """Returns a list of attribute strings, found in the specified | |
135 | object, which represent callable attributes""" | |
136 | ||
137 | return [member for member in dir(obj) | |
138 | if not member.startswith('_') and | |
139 | callable(getattr(obj, member))] | |
140 | ||
141 | def remove_duplicates(lst): | |
142 | """remove_duplicates([2,2,2,1,3,3]) => [3,1,2] | |
143 | ||
144 | Returns a copy of a list without duplicates. Every list | |
145 | item must be hashable and the order of the items in the | |
146 | resulting list is not defined. | |
147 | """ | |
148 | u = {} | |
149 | for x in lst: | |
150 | u[x] = 1 | |
151 | ||
152 | return u.keys() | |
153 | ||
154 | class SimpleXMLRPCDispatcher: | |
155 | """Mix-in class that dispatches XML-RPC requests. | |
156 | ||
157 | This class is used to register XML-RPC method handlers | |
158 | and then to dispatch them. There should never be any | |
159 | reason to instantiate this class directly. | |
160 | """ | |
161 | ||
162 | def __init__(self): | |
163 | self.funcs = {} | |
164 | self.instance = None | |
165 | ||
166 | def register_instance(self, instance, allow_dotted_names=False): | |
167 | """Registers an instance to respond to XML-RPC requests. | |
168 | ||
169 | Only one instance can be installed at a time. | |
170 | ||
171 | If the registered instance has a _dispatch method then that | |
172 | method will be called with the name of the XML-RPC method and | |
173 | its parameters as a tuple | |
174 | e.g. instance._dispatch('add',(2,3)) | |
175 | ||
176 | If the registered instance does not have a _dispatch method | |
177 | then the instance will be searched to find a matching method | |
178 | and, if found, will be called. Methods beginning with an '_' | |
179 | are considered private and will not be called by | |
180 | SimpleXMLRPCServer. | |
181 | ||
182 | If a registered function matches a XML-RPC request, then it | |
183 | will be called instead of the registered instance. | |
184 | ||
185 | If the optional allow_dotted_names argument is true and the | |
186 | instance does not have a _dispatch method, method names | |
187 | containing dots are supported and resolved, as long as none of | |
188 | the name segments start with an '_'. | |
189 | ||
190 | *** SECURITY WARNING: *** | |
191 | ||
192 | Enabling the allow_dotted_names options allows intruders | |
193 | to access your module's global variables and may allow | |
194 | intruders to execute arbitrary code on your machine. Only | |
195 | use this option on a secure, closed network. | |
196 | ||
197 | """ | |
198 | ||
199 | self.instance = instance | |
200 | self.allow_dotted_names = allow_dotted_names | |
201 | ||
202 | def register_function(self, function, name = None): | |
203 | """Registers a function to respond to XML-RPC requests. | |
204 | ||
205 | The optional name argument can be used to set a Unicode name | |
206 | for the function. | |
207 | """ | |
208 | ||
209 | if name is None: | |
210 | name = function.__name__ | |
211 | self.funcs[name] = function | |
212 | ||
213 | def register_introspection_functions(self): | |
214 | """Registers the XML-RPC introspection methods in the system | |
215 | namespace. | |
216 | ||
217 | see http://xmlrpc.usefulinc.com/doc/reserved.html | |
218 | """ | |
219 | ||
220 | self.funcs.update({'system.listMethods' : self.system_listMethods, | |
221 | 'system.methodSignature' : self.system_methodSignature, | |
222 | 'system.methodHelp' : self.system_methodHelp}) | |
223 | ||
224 | def register_multicall_functions(self): | |
225 | """Registers the XML-RPC multicall method in the system | |
226 | namespace. | |
227 | ||
228 | see http://www.xmlrpc.com/discuss/msgReader$1208""" | |
229 | ||
230 | self.funcs.update({'system.multicall' : self.system_multicall}) | |
231 | ||
232 | def _marshaled_dispatch(self, data, dispatch_method = None): | |
233 | """Dispatches an XML-RPC method from marshalled (XML) data. | |
234 | ||
235 | XML-RPC methods are dispatched from the marshalled (XML) data | |
236 | using the _dispatch method and the result is returned as | |
237 | marshalled data. For backwards compatibility, a dispatch | |
238 | function can be provided as an argument (see comment in | |
239 | SimpleXMLRPCRequestHandler.do_POST) but overriding the | |
240 | existing method through subclassing is the prefered means | |
241 | of changing method dispatch behavior. | |
242 | """ | |
243 | ||
244 | params, method = xmlrpclib.loads(data) | |
245 | ||
246 | # generate response | |
247 | try: | |
248 | if dispatch_method is not None: | |
249 | response = dispatch_method(method, params) | |
250 | else: | |
251 | response = self._dispatch(method, params) | |
252 | # wrap response in a singleton tuple | |
253 | response = (response,) | |
254 | response = xmlrpclib.dumps(response, methodresponse=1) | |
255 | except Fault, fault: | |
256 | response = xmlrpclib.dumps(fault) | |
257 | except: | |
258 | # report exception back to server | |
259 | response = xmlrpclib.dumps( | |
260 | xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)) | |
261 | ) | |
262 | ||
263 | return response | |
264 | ||
265 | def system_listMethods(self): | |
266 | """system.listMethods() => ['add', 'subtract', 'multiple'] | |
267 | ||
268 | Returns a list of the methods supported by the server.""" | |
269 | ||
270 | methods = self.funcs.keys() | |
271 | if self.instance is not None: | |
272 | # Instance can implement _listMethod to return a list of | |
273 | # methods | |
274 | if hasattr(self.instance, '_listMethods'): | |
275 | methods = remove_duplicates( | |
276 | methods + self.instance._listMethods() | |
277 | ) | |
278 | # if the instance has a _dispatch method then we | |
279 | # don't have enough information to provide a list | |
280 | # of methods | |
281 | elif not hasattr(self.instance, '_dispatch'): | |
282 | methods = remove_duplicates( | |
283 | methods + list_public_methods(self.instance) | |
284 | ) | |
285 | methods.sort() | |
286 | return methods | |
287 | ||
288 | def system_methodSignature(self, method_name): | |
289 | """system.methodSignature('add') => [double, int, int] | |
290 | ||
291 | Returns a list describing the signature of the method. In the | |
292 | above example, the add method takes two integers as arguments | |
293 | and returns a double result. | |
294 | ||
295 | This server does NOT support system.methodSignature.""" | |
296 | ||
297 | # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html | |
298 | ||
299 | return 'signatures not supported' | |
300 | ||
301 | def system_methodHelp(self, method_name): | |
302 | """system.methodHelp('add') => "Adds two integers together" | |
303 | ||
304 | Returns a string containing documentation for the specified method.""" | |
305 | ||
306 | method = None | |
307 | if self.funcs.has_key(method_name): | |
308 | method = self.funcs[method_name] | |
309 | elif self.instance is not None: | |
310 | # Instance can implement _methodHelp to return help for a method | |
311 | if hasattr(self.instance, '_methodHelp'): | |
312 | return self.instance._methodHelp(method_name) | |
313 | # if the instance has a _dispatch method then we | |
314 | # don't have enough information to provide help | |
315 | elif not hasattr(self.instance, '_dispatch'): | |
316 | try: | |
317 | method = resolve_dotted_attribute( | |
318 | self.instance, | |
319 | method_name, | |
320 | self.allow_dotted_names | |
321 | ) | |
322 | except AttributeError: | |
323 | pass | |
324 | ||
325 | # Note that we aren't checking that the method actually | |
326 | # be a callable object of some kind | |
327 | if method is None: | |
328 | return "" | |
329 | else: | |
330 | import pydoc | |
331 | return pydoc.getdoc(method) | |
332 | ||
333 | def system_multicall(self, call_list): | |
334 | """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \ | |
335 | [[4], ...] | |
336 | ||
337 | Allows the caller to package multiple XML-RPC calls into a single | |
338 | request. | |
339 | ||
340 | See http://www.xmlrpc.com/discuss/msgReader$1208 | |
341 | """ | |
342 | ||
343 | results = [] | |
344 | for call in call_list: | |
345 | method_name = call['methodName'] | |
346 | params = call['params'] | |
347 | ||
348 | try: | |
349 | # XXX A marshalling error in any response will fail the entire | |
350 | # multicall. If someone cares they should fix this. | |
351 | results.append([self._dispatch(method_name, params)]) | |
352 | except Fault, fault: | |
353 | results.append( | |
354 | {'faultCode' : fault.faultCode, | |
355 | 'faultString' : fault.faultString} | |
356 | ) | |
357 | except: | |
358 | results.append( | |
359 | {'faultCode' : 1, | |
360 | 'faultString' : "%s:%s" % (sys.exc_type, sys.exc_value)} | |
361 | ) | |
362 | return results | |
363 | ||
364 | def _dispatch(self, method, params): | |
365 | """Dispatches the XML-RPC method. | |
366 | ||
367 | XML-RPC calls are forwarded to a registered function that | |
368 | matches the called XML-RPC method name. If no such function | |
369 | exists then the call is forwarded to the registered instance, | |
370 | if available. | |
371 | ||
372 | If the registered instance has a _dispatch method then that | |
373 | method will be called with the name of the XML-RPC method and | |
374 | its parameters as a tuple | |
375 | e.g. instance._dispatch('add',(2,3)) | |
376 | ||
377 | If the registered instance does not have a _dispatch method | |
378 | then the instance will be searched to find a matching method | |
379 | and, if found, will be called. | |
380 | ||
381 | Methods beginning with an '_' are considered private and will | |
382 | not be called. | |
383 | """ | |
384 | ||
385 | func = None | |
386 | try: | |
387 | # check to see if a matching function has been registered | |
388 | func = self.funcs[method] | |
389 | except KeyError: | |
390 | if self.instance is not None: | |
391 | # check for a _dispatch method | |
392 | if hasattr(self.instance, '_dispatch'): | |
393 | return self.instance._dispatch(method, params) | |
394 | else: | |
395 | # call instance method directly | |
396 | try: | |
397 | func = resolve_dotted_attribute( | |
398 | self.instance, | |
399 | method, | |
400 | self.allow_dotted_names | |
401 | ) | |
402 | except AttributeError: | |
403 | pass | |
404 | ||
405 | if func is not None: | |
406 | return func(*params) | |
407 | else: | |
408 | raise Exception('method "%s" is not supported' % method) | |
409 | ||
410 | class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): | |
411 | """Simple XML-RPC request handler class. | |
412 | ||
413 | Handles all HTTP POST requests and attempts to decode them as | |
414 | XML-RPC requests. | |
415 | """ | |
416 | ||
417 | def do_POST(self): | |
418 | """Handles the HTTP POST request. | |
419 | ||
420 | Attempts to interpret all HTTP POST requests as XML-RPC calls, | |
421 | which are forwarded to the server's _dispatch method for handling. | |
422 | """ | |
423 | ||
424 | try: | |
425 | # get arguments | |
426 | data = self.rfile.read(int(self.headers["content-length"])) | |
427 | # In previous versions of SimpleXMLRPCServer, _dispatch | |
428 | # could be overridden in this class, instead of in | |
429 | # SimpleXMLRPCDispatcher. To maintain backwards compatibility, | |
430 | # check to see if a subclass implements _dispatch and dispatch | |
431 | # using that method if present. | |
432 | response = self.server._marshaled_dispatch( | |
433 | data, getattr(self, '_dispatch', None) | |
434 | ) | |
435 | except: # This should only happen if the module is buggy | |
436 | # internal error, report as HTTP server error | |
437 | self.send_response(500) | |
438 | self.end_headers() | |
439 | else: | |
440 | # got a valid XML RPC response | |
441 | self.send_response(200) | |
442 | self.send_header("Content-type", "text/xml") | |
443 | self.send_header("Content-length", str(len(response))) | |
444 | self.end_headers() | |
445 | self.wfile.write(response) | |
446 | ||
447 | # shut down the connection | |
448 | self.wfile.flush() | |
449 | self.connection.shutdown(1) | |
450 | ||
451 | def log_request(self, code='-', size='-'): | |
452 | """Selectively log an accepted request.""" | |
453 | ||
454 | if self.server.logRequests: | |
455 | BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size) | |
456 | ||
457 | class SimpleXMLRPCServer(SocketServer.TCPServer, | |
458 | SimpleXMLRPCDispatcher): | |
459 | """Simple XML-RPC server. | |
460 | ||
461 | Simple XML-RPC server that allows functions and a single instance | |
462 | to be installed to handle requests. The default implementation | |
463 | attempts to dispatch XML-RPC calls to the functions or instance | |
464 | installed in the server. Override the _dispatch method inhereted | |
465 | from SimpleXMLRPCDispatcher to change this behavior. | |
466 | """ | |
467 | ||
468 | def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, | |
469 | logRequests=1): | |
470 | self.logRequests = logRequests | |
471 | ||
472 | SimpleXMLRPCDispatcher.__init__(self) | |
473 | SocketServer.TCPServer.__init__(self, addr, requestHandler) | |
474 | ||
475 | class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): | |
476 | """Simple handler for XML-RPC data passed through CGI.""" | |
477 | ||
478 | def __init__(self): | |
479 | SimpleXMLRPCDispatcher.__init__(self) | |
480 | ||
481 | def handle_xmlrpc(self, request_text): | |
482 | """Handle a single XML-RPC request""" | |
483 | ||
484 | response = self._marshaled_dispatch(request_text) | |
485 | ||
486 | print 'Content-Type: text/xml' | |
487 | print 'Content-Length: %d' % len(response) | |
488 | ||
489 | sys.stdout.write(response) | |
490 | ||
491 | def handle_get(self): | |
492 | """Handle a single HTTP GET request. | |
493 | ||
494 | Default implementation indicates an error because | |
495 | XML-RPC uses the POST method. | |
496 | """ | |
497 | ||
498 | code = 400 | |
499 | message, explain = \ | |
500 | BaseHTTPServer.BaseHTTPRequestHandler.responses[code] | |
501 | ||
502 | response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \ | |
503 | { | |
504 | 'code' : code, | |
505 | 'message' : message, | |
506 | 'explain' : explain | |
507 | } | |
508 | print 'Status: %d %s' % (code, message) | |
509 | print 'Content-Type: text/html' | |
510 | print 'Content-Length: %d' % len(response) | |
511 | ||
512 | sys.stdout.write(response) | |
513 | ||
514 | def handle_request(self, request_text = None): | |
515 | """Handle a single XML-RPC request passed through a CGI post method. | |
516 | ||
517 | If no XML data is given then it is read from stdin. The resulting | |
518 | XML-RPC response is printed to stdout along with the correct HTTP | |
519 | headers. | |
520 | """ | |
521 | ||
522 | if request_text is None and \ | |
523 | os.environ.get('REQUEST_METHOD', None) == 'GET': | |
524 | self.handle_get() | |
525 | else: | |
526 | # POST data is normally available through stdin | |
527 | if request_text is None: | |
528 | request_text = sys.stdin.read() | |
529 | ||
530 | self.handle_xmlrpc(request_text) | |
531 | ||
532 | if __name__ == '__main__': | |
533 | server = SimpleXMLRPCServer(("localhost", 8000)) | |
534 | server.register_function(pow) | |
535 | server.register_function(lambda x,y: x+y, 'add') | |
536 | server.serve_forever() |