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 an 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.register_multicall_functions() 708 server.serve_forever() 709