source: python/trunk/Lib/SimpleXMLRPCServer.py@ 394

Last change on this file since 394 was 391, checked in by dmik, 11 years ago

python: Merge vendor 2.7.6 to trunk.

  • Property svn:eol-style set to native
File size: 25.2 KB
Line 
1r"""Simple XML-RPC Server.
2
3This module can be used to create simple XML-RPC servers
4by creating a server and either installing functions, a
5class instance, or by extending the SimpleXMLRPCServer
6class.
7
8It can also be used to handle XML-RPC requests in a CGI
9environment using CGIXMLRPCRequestHandler.
10
11A list of possible usage patterns follows:
12
131. Install functions:
14
15server = SimpleXMLRPCServer(("localhost", 8000))
16server.register_function(pow)
17server.register_function(lambda x,y: x+y, 'add')
18server.serve_forever()
19
202. Install an instance:
21
22class 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
36server = SimpleXMLRPCServer(("localhost", 8000))
37server.register_introspection_functions()
38server.register_instance(MyFuncs())
39server.serve_forever()
40
413. Install an instance with custom dispatch method:
42
43class 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
67server = SimpleXMLRPCServer(("localhost", 8000))
68server.register_introspection_functions()
69server.register_instance(Math())
70server.serve_forever()
71
724. Subclass SimpleXMLRPCServer:
73
74class 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
89server = MathServer(("localhost", 8000))
90server.serve_forever()
91
925. CGI script:
93
94server = CGIXMLRPCRequestHandler()
95server.register_function(pow)
96server.handle_request()
97"""
98
99# Written by Brian Quinlan (brian@sweetapp.com).
100# Based on code written by Fredrik Lundh.
101
102import xmlrpclib
103from xmlrpclib import Fault
104import SocketServer
105import BaseHTTPServer
106import sys
107import os
108import traceback
109import re
110try:
111 import fcntl
112except ImportError:
113 fcntl = None
114
115def resolve_dotted_attribute(obj, attr, allow_dotted_names=True):
116 """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d
117
118 Resolves a dotted attribute name to an object. Raises
119 an AttributeError if any attribute in the chain starts with a '_'.
120
121 If the optional allow_dotted_names argument is false, dots are not
122 supported and this function operates similar to getattr(obj, attr).
123 """
124
125 if allow_dotted_names:
126 attrs = attr.split('.')
127 else:
128 attrs = [attr]
129
130 for i in attrs:
131 if i.startswith('_'):
132 raise AttributeError(
133 'attempt to access private attribute "%s"' % i
134 )
135 else:
136 obj = getattr(obj,i)
137 return obj
138
139def list_public_methods(obj):
140 """Returns a list of attribute strings, found in the specified
141 object, which represent callable attributes"""
142
143 return [member for member in dir(obj)
144 if not member.startswith('_') and
145 hasattr(getattr(obj, member), '__call__')]
146
147def remove_duplicates(lst):
148 """remove_duplicates([2,2,2,1,3,3]) => [3,1,2]
149
150 Returns a copy of a list without duplicates. Every list
151 item must be hashable and the order of the items in the
152 resulting list is not defined.
153 """
154 u = {}
155 for x in lst:
156 u[x] = 1
157
158 return u.keys()
159
160class SimpleXMLRPCDispatcher:
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 self.funcs = {}
171 self.instance = None
172 self.allow_none = allow_none
173 self.encoding = encoding
174
175 def register_instance(self, instance, allow_dotted_names=False):
176 """Registers an instance to respond to XML-RPC requests.
177
178 Only one instance can be installed at a time.
179
180 If the registered instance has a _dispatch method then that
181 method will be called with the name of the XML-RPC method and
182 its parameters as a tuple
183 e.g. instance._dispatch('add',(2,3))
184
185 If the registered instance does not have a _dispatch method
186 then the instance will be searched to find a matching method
187 and, if found, will be called. Methods beginning with an '_'
188 are considered private and will not be called by
189 SimpleXMLRPCServer.
190
191 If a registered function matches a XML-RPC request, then it
192 will be called instead of the registered instance.
193
194 If the optional allow_dotted_names argument is true and the
195 instance does not have a _dispatch method, method names
196 containing dots are supported and resolved, as long as none of
197 the name segments start with an '_'.
198
199 *** SECURITY WARNING: ***
200
201 Enabling the allow_dotted_names options allows intruders
202 to access your module's global variables and may allow
203 intruders to execute arbitrary code on your machine. Only
204 use this option on a secure, closed network.
205
206 """
207
208 self.instance = instance
209 self.allow_dotted_names = allow_dotted_names
210
211 def register_function(self, function, name = None):
212 """Registers a function to respond to XML-RPC requests.
213
214 The optional name argument can be used to set a Unicode name
215 for the function.
216 """
217
218 if name is None:
219 name = function.__name__
220 self.funcs[name] = function
221
222 def register_introspection_functions(self):
223 """Registers the XML-RPC introspection methods in the system
224 namespace.
225
226 see http://xmlrpc.usefulinc.com/doc/reserved.html
227 """
228
229 self.funcs.update({'system.listMethods' : self.system_listMethods,
230 'system.methodSignature' : self.system_methodSignature,
231 'system.methodHelp' : self.system_methodHelp})
232
233 def register_multicall_functions(self):
234 """Registers the XML-RPC multicall method in the system
235 namespace.
236
237 see http://www.xmlrpc.com/discuss/msgReader$1208"""
238
239 self.funcs.update({'system.multicall' : self.system_multicall})
240
241 def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
242 """Dispatches an XML-RPC method from marshalled (XML) data.
243
244 XML-RPC methods are dispatched from the marshalled (XML) data
245 using the _dispatch method and the result is returned as
246 marshalled data. For backwards compatibility, a dispatch
247 function can be provided as an argument (see comment in
248 SimpleXMLRPCRequestHandler.do_POST) but overriding the
249 existing method through subclassing is the preferred means
250 of changing method dispatch behavior.
251 """
252
253 try:
254 params, method = xmlrpclib.loads(data)
255
256 # generate response
257 if dispatch_method is not None:
258 response = dispatch_method(method, params)
259 else:
260 response = self._dispatch(method, params)
261 # wrap response in a singleton tuple
262 response = (response,)
263 response = xmlrpclib.dumps(response, methodresponse=1,
264 allow_none=self.allow_none, encoding=self.encoding)
265 except Fault, fault:
266 response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
267 encoding=self.encoding)
268 except:
269 # report exception back to server
270 exc_type, exc_value, exc_tb = sys.exc_info()
271 response = xmlrpclib.dumps(
272 xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
273 encoding=self.encoding, allow_none=self.allow_none,
274 )
275
276 return response
277
278 def system_listMethods(self):
279 """system.listMethods() => ['add', 'subtract', 'multiple']
280
281 Returns a list of the methods supported by the server."""
282
283 methods = self.funcs.keys()
284 if self.instance is not None:
285 # Instance can implement _listMethod to return a list of
286 # methods
287 if hasattr(self.instance, '_listMethods'):
288 methods = remove_duplicates(
289 methods + self.instance._listMethods()
290 )
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 = remove_duplicates(
296 methods + list_public_methods(self.instance)
297 )
298 methods.sort()
299 return methods
300
301 def system_methodSignature(self, method_name):
302 """system.methodSignature('add') => [double, int, int]
303
304 Returns a list describing the signature of the method. In the
305 above example, the add method takes two integers as arguments
306 and returns a double result.
307
308 This server does NOT support system.methodSignature."""
309
310 # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html
311
312 return 'signatures not supported'
313
314 def system_methodHelp(self, method_name):
315 """system.methodHelp('add') => "Adds two integers together"
316
317 Returns a string containing documentation for the specified method."""
318
319 method = None
320 if method_name in self.funcs:
321 method = self.funcs[method_name]
322 elif self.instance is not None:
323 # Instance can implement _methodHelp to return help for a method
324 if hasattr(self.instance, '_methodHelp'):
325 return self.instance._methodHelp(method_name)
326 # if the instance has a _dispatch method then we
327 # don't have enough information to provide help
328 elif not hasattr(self.instance, '_dispatch'):
329 try:
330 method = resolve_dotted_attribute(
331 self.instance,
332 method_name,
333 self.allow_dotted_names
334 )
335 except AttributeError:
336 pass
337
338 # Note that we aren't checking that the method actually
339 # be a callable object of some kind
340 if method is None:
341 return ""
342 else:
343 import pydoc
344 return pydoc.getdoc(method)
345
346 def system_multicall(self, call_list):
347 """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \
348[[4], ...]
349
350 Allows the caller to package multiple XML-RPC calls into a single
351 request.
352
353 See http://www.xmlrpc.com/discuss/msgReader$1208
354 """
355
356 results = []
357 for call in call_list:
358 method_name = call['methodName']
359 params = call['params']
360
361 try:
362 # XXX A marshalling error in any response will fail the entire
363 # multicall. If someone cares they should fix this.
364 results.append([self._dispatch(method_name, params)])
365 except Fault, fault:
366 results.append(
367 {'faultCode' : fault.faultCode,
368 'faultString' : fault.faultString}
369 )
370 except:
371 exc_type, exc_value, exc_tb = sys.exc_info()
372 results.append(
373 {'faultCode' : 1,
374 'faultString' : "%s:%s" % (exc_type, exc_value)}
375 )
376 return results
377
378 def _dispatch(self, method, params):
379 """Dispatches the XML-RPC method.
380
381 XML-RPC calls are forwarded to a registered function that
382 matches the called XML-RPC method name. If no such function
383 exists then the call is forwarded to the registered instance,
384 if available.
385
386 If the registered instance has a _dispatch method then that
387 method will be called with the name of the XML-RPC method and
388 its parameters as a tuple
389 e.g. instance._dispatch('add',(2,3))
390
391 If the registered instance does not have a _dispatch method
392 then the instance will be searched to find a matching method
393 and, if found, will be called.
394
395 Methods beginning with an '_' are considered private and will
396 not be called.
397 """
398
399 func = None
400 try:
401 # check to see if a matching function has been registered
402 func = self.funcs[method]
403 except KeyError:
404 if self.instance is not None:
405 # check for a _dispatch method
406 if hasattr(self.instance, '_dispatch'):
407 return self.instance._dispatch(method, params)
408 else:
409 # call instance method directly
410 try:
411 func = resolve_dotted_attribute(
412 self.instance,
413 method,
414 self.allow_dotted_names
415 )
416 except AttributeError:
417 pass
418
419 if func is not None:
420 return func(*params)
421 else:
422 raise Exception('method "%s" is not supported' % method)
423
424class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
425 """Simple XML-RPC request handler class.
426
427 Handles all HTTP POST requests and attempts to decode them as
428 XML-RPC requests.
429 """
430
431 # Class attribute listing the accessible path components;
432 # paths not on this list will result in a 404 error.
433 rpc_paths = ('/', '/RPC2')
434
435 #if not None, encode responses larger than this, if possible
436 encode_threshold = 1400 #a common MTU
437
438 #Override form StreamRequestHandler: full buffering of output
439 #and no Nagle.
440 wbufsize = -1
441 disable_nagle_algorithm = True
442
443 # a re to match a gzip Accept-Encoding
444 aepattern = re.compile(r"""
445 \s* ([^\s;]+) \s* #content-coding
446 (;\s* q \s*=\s* ([0-9\.]+))? #q
447 """, re.VERBOSE | re.IGNORECASE)
448
449 def accept_encodings(self):
450 r = {}
451 ae = self.headers.get("Accept-Encoding", "")
452 for e in ae.split(","):
453 match = self.aepattern.match(e)
454 if match:
455 v = match.group(3)
456 v = float(v) if v else 1.0
457 r[match.group(1)] = v
458 return r
459
460 def is_rpc_path_valid(self):
461 if self.rpc_paths:
462 return self.path in self.rpc_paths
463 else:
464 # If .rpc_paths is empty, just assume all paths are legal
465 return True
466
467 def do_POST(self):
468 """Handles the HTTP POST request.
469
470 Attempts to interpret all HTTP POST requests as XML-RPC calls,
471 which are forwarded to the server's _dispatch method for handling.
472 """
473
474 # Check that the path is legal
475 if not self.is_rpc_path_valid():
476 self.report_404()
477 return
478
479 try:
480 # Get arguments by reading body of request.
481 # We read this in chunks to avoid straining
482 # socket.read(); around the 10 or 15Mb mark, some platforms
483 # begin to have problems (bug #792570).
484 max_chunk_size = 10*1024*1024
485 size_remaining = int(self.headers["content-length"])
486 L = []
487 while size_remaining:
488 chunk_size = min(size_remaining, max_chunk_size)
489 chunk = self.rfile.read(chunk_size)
490 if not chunk:
491 break
492 L.append(chunk)
493 size_remaining -= len(L[-1])
494 data = ''.join(L)
495
496 data = self.decode_request_content(data)
497 if data is None:
498 return #response has been sent
499
500 # In previous versions of SimpleXMLRPCServer, _dispatch
501 # could be overridden in this class, instead of in
502 # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
503 # check to see if a subclass implements _dispatch and dispatch
504 # using that method if present.
505 response = self.server._marshaled_dispatch(
506 data, getattr(self, '_dispatch', None), self.path
507 )
508 except Exception, e: # This should only happen if the module is buggy
509 # internal error, report as HTTP server error
510 self.send_response(500)
511
512 # Send information about the exception if requested
513 if hasattr(self.server, '_send_traceback_header') and \
514 self.server._send_traceback_header:
515 self.send_header("X-exception", str(e))
516 self.send_header("X-traceback", traceback.format_exc())
517
518 self.send_header("Content-length", "0")
519 self.end_headers()
520 else:
521 # got a valid XML RPC response
522 self.send_response(200)
523 self.send_header("Content-type", "text/xml")
524 if self.encode_threshold is not None:
525 if len(response) > self.encode_threshold:
526 q = self.accept_encodings().get("gzip", 0)
527 if q:
528 try:
529 response = xmlrpclib.gzip_encode(response)
530 self.send_header("Content-Encoding", "gzip")
531 except NotImplementedError:
532 pass
533 self.send_header("Content-length", str(len(response)))
534 self.end_headers()
535 self.wfile.write(response)
536
537 def decode_request_content(self, data):
538 #support gzip encoding of request
539 encoding = self.headers.get("content-encoding", "identity").lower()
540 if encoding == "identity":
541 return data
542 if encoding == "gzip":
543 try:
544 return xmlrpclib.gzip_decode(data)
545 except NotImplementedError:
546 self.send_response(501, "encoding %r not supported" % encoding)
547 except ValueError:
548 self.send_response(400, "error decoding gzip content")
549 else:
550 self.send_response(501, "encoding %r not supported" % encoding)
551 self.send_header("Content-length", "0")
552 self.end_headers()
553
554 def report_404 (self):
555 # Report a 404 error
556 self.send_response(404)
557 response = 'No such page'
558 self.send_header("Content-type", "text/plain")
559 self.send_header("Content-length", str(len(response)))
560 self.end_headers()
561 self.wfile.write(response)
562
563 def log_request(self, code='-', size='-'):
564 """Selectively log an accepted request."""
565
566 if self.server.logRequests:
567 BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size)
568
569class SimpleXMLRPCServer(SocketServer.TCPServer,
570 SimpleXMLRPCDispatcher):
571 """Simple XML-RPC server.
572
573 Simple XML-RPC server that allows functions and a single instance
574 to be installed to handle requests. The default implementation
575 attempts to dispatch XML-RPC calls to the functions or instance
576 installed in the server. Override the _dispatch method inhereted
577 from SimpleXMLRPCDispatcher to change this behavior.
578 """
579
580 allow_reuse_address = True
581
582 # Warning: this is for debugging purposes only! Never set this to True in
583 # production code, as will be sending out sensitive information (exception
584 # and stack trace details) when exceptions are raised inside
585 # SimpleXMLRPCRequestHandler.do_POST
586 _send_traceback_header = False
587
588 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
589 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
590 self.logRequests = logRequests
591
592 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
593 SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate)
594
595 # [Bug #1222790] If possible, set close-on-exec flag; if a
596 # method spawns a subprocess, the subprocess shouldn't have
597 # the listening socket open.
598 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
599 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
600 flags |= fcntl.FD_CLOEXEC
601 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
602
603class MultiPathXMLRPCServer(SimpleXMLRPCServer):
604 """Multipath XML-RPC Server
605 This specialization of SimpleXMLRPCServer allows the user to create
606 multiple Dispatcher instances and assign them to different
607 HTTP request paths. This makes it possible to run two or more
608 'virtual XML-RPC servers' at the same port.
609 Make sure that the requestHandler accepts the paths in question.
610 """
611 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
612 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
613
614 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none,
615 encoding, bind_and_activate)
616 self.dispatchers = {}
617 self.allow_none = allow_none
618 self.encoding = encoding
619
620 def add_dispatcher(self, path, dispatcher):
621 self.dispatchers[path] = dispatcher
622 return dispatcher
623
624 def get_dispatcher(self, path):
625 return self.dispatchers[path]
626
627 def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
628 try:
629 response = self.dispatchers[path]._marshaled_dispatch(
630 data, dispatch_method, path)
631 except:
632 # report low level exception back to server
633 # (each dispatcher should have handled their own
634 # exceptions)
635 exc_type, exc_value = sys.exc_info()[:2]
636 response = xmlrpclib.dumps(
637 xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)),
638 encoding=self.encoding, allow_none=self.allow_none)
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):
645 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
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.write(response)
656
657 def handle_get(self):
658 """Handle a single HTTP GET request.
659
660 Default implementation indicates an error because
661 XML-RPC uses the POST method.
662 """
663
664 code = 400
665 message, explain = \
666 BaseHTTPServer.BaseHTTPRequestHandler.responses[code]
667
668 response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \
669 {
670 'code' : code,
671 'message' : message,
672 'explain' : explain
673 }
674 print 'Status: %d %s' % (code, message)
675 print 'Content-Type: %s' % BaseHTTPServer.DEFAULT_ERROR_CONTENT_TYPE
676 print 'Content-Length: %d' % len(response)
677 print
678 sys.stdout.write(response)
679
680 def handle_request(self, request_text = None):
681 """Handle a single XML-RPC request passed through a CGI post method.
682
683 If no XML data is given then it is read from stdin. The resulting
684 XML-RPC response is printed to stdout along with the correct HTTP
685 headers.
686 """
687
688 if request_text is None and \
689 os.environ.get('REQUEST_METHOD', None) == 'GET':
690 self.handle_get()
691 else:
692 # POST data is normally available through stdin
693 try:
694 length = int(os.environ.get('CONTENT_LENGTH', None))
695 except (TypeError, ValueError):
696 length = -1
697 if request_text is None:
698 request_text = sys.stdin.read(length)
699
700 self.handle_xmlrpc(request_text)
701
702if __name__ == '__main__':
703 print 'Running XML-RPC server on port 8000'
704 server = SimpleXMLRPCServer(("localhost", 8000))
705 server.register_function(pow)
706 server.register_function(lambda x,y: x+y, 'add')
707 server.serve_forever()
Note: See TracBrowser for help on using the repository browser.