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 (at] 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 import traceback 109 import re 110 try: 111 import fcntl 112 except ImportError: 113 fcntl = None 114 115 def 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 139 def 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 147 def 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 160 class 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 424 class 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 L.append(self.rfile.read(chunk_size)) 490 size_remaining -= len(L[-1]) 491 data = ''.join(L) 492 493 data = self.decode_request_content(data) 494 if data is None: 495 return #response has been sent 496 497 # In previous versions of SimpleXMLRPCServer, _dispatch 498 # could be overridden in this class, instead of in 499 # SimpleXMLRPCDispatcher. To maintain backwards compatibility, 500 # check to see if a subclass implements _dispatch and dispatch 501 # using that method if present. 502 response = self.server._marshaled_dispatch( 503 data, getattr(self, '_dispatch', None), self.path 504 ) 505 except Exception, e: # This should only happen if the module is buggy 506 # internal error, report as HTTP server error 507 self.send_response(500) 508 509 # Send information about the exception if requested 510 if hasattr(self.server, '_send_traceback_header') and \ 511 self.server._send_traceback_header: 512 self.send_header("X-exception", str(e)) 513 self.send_header("X-traceback", traceback.format_exc()) 514 515 self.send_header("Content-length", "0") 516 self.end_headers() 517 else: 518 # got a valid XML RPC response 519 self.send_response(200) 520 self.send_header("Content-type", "text/xml") 521 if self.encode_threshold is not None: 522 if len(response) > self.encode_threshold: 523 q = self.accept_encodings().get("gzip", 0) 524 if q: 525 try: 526 response = xmlrpclib.gzip_encode(response) 527 self.send_header("Content-Encoding", "gzip") 528 except NotImplementedError: 529 pass 530 self.send_header("Content-length", str(len(response))) 531 self.end_headers() 532 self.wfile.write(response) 533 534 def decode_request_content(self, data): 535 #support gzip encoding of request 536 encoding = self.headers.get("content-encoding", "identity").lower() 537 if encoding == "identity": 538 return data 539 if encoding == "gzip": 540 try: 541 return xmlrpclib.gzip_decode(data) 542 except NotImplementedError: 543 self.send_response(501, "encoding %r not supported" % encoding) 544 except ValueError: 545 self.send_response(400, "error decoding gzip content") 546 else: 547 self.send_response(501, "encoding %r not supported" % encoding) 548 self.send_header("Content-length", "0") 549 self.end_headers() 550 551 def report_404 (self): 552 # Report a 404 error 553 self.send_response(404) 554 response = 'No such page' 555 self.send_header("Content-type", "text/plain") 556 self.send_header("Content-length", str(len(response))) 557 self.end_headers() 558 self.wfile.write(response) 559 560 def log_request(self, code='-', size='-'): 561 """Selectively log an accepted request.""" 562 563 if self.server.logRequests: 564 BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size) 565 566 class SimpleXMLRPCServer(SocketServer.TCPServer, 567 SimpleXMLRPCDispatcher): 568 """Simple XML-RPC server. 569 570 Simple XML-RPC server that allows functions and a single instance 571 to be installed to handle requests. The default implementation 572 attempts to dispatch XML-RPC calls to the functions or instance 573 installed in the server. Override the _dispatch method inhereted 574 from SimpleXMLRPCDispatcher to change this behavior. 575 """ 576 577 allow_reuse_address = True 578 579 # Warning: this is for debugging purposes only! Never set this to True in 580 # production code, as will be sending out sensitive information (exception 581 # and stack trace details) when exceptions are raised inside 582 # SimpleXMLRPCRequestHandler.do_POST 583 _send_traceback_header = False 584 585 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, 586 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True): 587 self.logRequests = logRequests 588 589 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) 590 SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate) 591 592 # [Bug #1222790] If possible, set close-on-exec flag; if a 593 # method spawns a subprocess, the subprocess shouldn't have 594 # the listening socket open. 595 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): 596 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) 597 flags |= fcntl.FD_CLOEXEC 598 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) 599 600 class MultiPathXMLRPCServer(SimpleXMLRPCServer): 601 """Multipath XML-RPC Server 602 This specialization of SimpleXMLRPCServer allows the user to create 603 multiple Dispatcher instances and assign them to different 604 HTTP request paths. This makes it possible to run two or more 605 'virtual XML-RPC servers' at the same port. 606 Make sure that the requestHandler accepts the paths in question. 607 """ 608 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, 609 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True): 610 611 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none, 612 encoding, bind_and_activate) 613 self.dispatchers = {} 614 self.allow_none = allow_none 615 self.encoding = encoding 616 617 def add_dispatcher(self, path, dispatcher): 618 self.dispatchers[path] = dispatcher 619 return dispatcher 620 621 def get_dispatcher(self, path): 622 return self.dispatchers[path] 623 624 def _marshaled_dispatch(self, data, dispatch_method = None, path = None): 625 try: 626 response = self.dispatchers[path]._marshaled_dispatch( 627 data, dispatch_method, path) 628 except: 629 # report low level exception back to server 630 # (each dispatcher should have handled their own 631 # exceptions) 632 exc_type, exc_value = sys.exc_info()[:2] 633 response = xmlrpclib.dumps( 634 xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)), 635 encoding=self.encoding, allow_none=self.allow_none) 636 return response 637 638 class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): 639 """Simple handler for XML-RPC data passed through CGI.""" 640 641 def __init__(self, allow_none=False, encoding=None): 642 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) 643 644 def handle_xmlrpc(self, request_text): 645 """Handle a single XML-RPC request""" 646 647 response = self._marshaled_dispatch(request_text) 648 649 print 'Content-Type: text/xml' 650 print 'Content-Length: %d' % len(response) 651 print 652 sys.stdout.write(response) 653 654 def handle_get(self): 655 """Handle a single HTTP GET request. 656 657 Default implementation indicates an error because 658 XML-RPC uses the POST method. 659 """ 660 661 code = 400 662 message, explain = \ 663 BaseHTTPServer.BaseHTTPRequestHandler.responses[code] 664 665 response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \ 666 { 667 'code' : code, 668 'message' : message, 669 'explain' : explain 670 } 671 print 'Status: %d %s' % (code, message) 672 print 'Content-Type: %s' % BaseHTTPServer.DEFAULT_ERROR_CONTENT_TYPE 673 print 'Content-Length: %d' % len(response) 674 print 675 sys.stdout.write(response) 676 677 def handle_request(self, request_text = None): 678 """Handle a single XML-RPC request passed through a CGI post method. 679 680 If no XML data is given then it is read from stdin. The resulting 681 XML-RPC response is printed to stdout along with the correct HTTP 682 headers. 683 """ 684 685 if request_text is None and \ 686 os.environ.get('REQUEST_METHOD', None) == 'GET': 687 self.handle_get() 688 else: 689 # POST data is normally available through stdin 690 try: 691 length = int(os.environ.get('CONTENT_LENGTH', None)) 692 except (TypeError, ValueError): 693 length = -1 694 if request_text is None: 695 request_text = sys.stdin.read(length) 696 697 self.handle_xmlrpc(request_text) 698 699 if __name__ == '__main__': 700 print 'Running XML-RPC server on port 8000' 701 server = SimpleXMLRPCServer(("localhost", 8000)) 702 server.register_function(pow) 703 server.register_function(lambda x,y: x+y, 'add') 704 server.serve_forever() 705