1r"""XML-RPC Servers. 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 11The Doc* classes can be used to create XML-RPC servers that 12serve pydoc-style documentation in response to HTTP 13GET requests. This documentation is dynamically generated 14based on the functions and methods registered with the 15server. 16 17A list of possible usage patterns follows: 18 191. Install functions: 20 21server = SimpleXMLRPCServer(("localhost", 8000)) 22server.register_function(pow) 23server.register_function(lambda x,y: x+y, 'add') 24server.serve_forever() 25 262. Install an instance: 27 28class MyFuncs: 29 def __init__(self): 30 # make all of the sys functions available through sys.func_name 31 import sys 32 self.sys = sys 33 def _listMethods(self): 34 # implement this method so that system.listMethods 35 # knows to advertise the sys methods 36 return list_public_methods(self) + \ 37 ['sys.' + method for method in list_public_methods(self.sys)] 38 def pow(self, x, y): return pow(x, y) 39 def add(self, x, y) : return x + y 40 41server = SimpleXMLRPCServer(("localhost", 8000)) 42server.register_introspection_functions() 43server.register_instance(MyFuncs()) 44server.serve_forever() 45 463. Install an instance with custom dispatch method: 47 48class Math: 49 def _listMethods(self): 50 # this method must be present for system.listMethods 51 # to work 52 return ['add', 'pow'] 53 def _methodHelp(self, method): 54 # this method must be present for system.methodHelp 55 # to work 56 if method == 'add': 57 return "add(2,3) => 5" 58 elif method == 'pow': 59 return "pow(x, y[, z]) => number" 60 else: 61 # By convention, return empty 62 # string if no help is available 63 return "" 64 def _dispatch(self, method, params): 65 if method == 'pow': 66 return pow(*params) 67 elif method == 'add': 68 return params[0] + params[1] 69 else: 70 raise ValueError('bad method') 71 72server = SimpleXMLRPCServer(("localhost", 8000)) 73server.register_introspection_functions() 74server.register_instance(Math()) 75server.serve_forever() 76 774. Subclass SimpleXMLRPCServer: 78 79class MathServer(SimpleXMLRPCServer): 80 def _dispatch(self, method, params): 81 try: 82 # We are forcing the 'export_' prefix on methods that are 83 # callable through XML-RPC to prevent potential security 84 # problems 85 func = getattr(self, 'export_' + method) 86 except AttributeError: 87 raise Exception('method "%s" is not supported' % method) 88 else: 89 return func(*params) 90 91 def export_add(self, x, y): 92 return x + y 93 94server = MathServer(("localhost", 8000)) 95server.serve_forever() 96 975. CGI script: 98 99server = CGIXMLRPCRequestHandler() 100server.register_function(pow) 101server.handle_request() 102""" 103 104# Written by Brian Quinlan (brian@sweetapp.com). 105# Based on code written by Fredrik Lundh. 106 107from xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode 108from http.server import BaseHTTPRequestHandler 109import http.server 110import socketserver 111import sys 112import os 113import re 114import pydoc 115import inspect 116import traceback 117try: 118 import fcntl 119except ImportError: 120 fcntl = None 121 122def resolve_dotted_attribute(obj, attr, allow_dotted_names=True): 123 """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d 124 125 Resolves a dotted attribute name to an object. Raises 126 an AttributeError if any attribute in the chain starts with a '_'. 127 128 If the optional allow_dotted_names argument is false, dots are not 129 supported and this function operates similar to getattr(obj, attr). 130 """ 131 132 if allow_dotted_names: 133 attrs = attr.split('.') 134 else: 135 attrs = [attr] 136 137 for i in attrs: 138 if i.startswith('_'): 139 raise AttributeError( 140 'attempt to access private attribute "%s"' % i 141 ) 142 else: 143 obj = getattr(obj,i) 144 return obj 145 146def list_public_methods(obj): 147 """Returns a list of attribute strings, found in the specified 148 object, which represent callable attributes""" 149 150 return [member for member in dir(obj) 151 if not member.startswith('_') and 152 callable(getattr(obj, member))] 153 154class 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. This class doesn't need to be 159 instanced directly when used by SimpleXMLRPCServer but it 160 can be instanced when used by the MultiPathXMLRPCServer 161 """ 162 163 def __init__(self, allow_none=False, encoding=None, 164 use_builtin_types=False): 165 self.funcs = {} 166 self.instance = None 167 self.allow_none = allow_none 168 self.encoding = encoding or 'utf-8' 169 self.use_builtin_types = use_builtin_types 170 171 def register_instance(self, instance, allow_dotted_names=False): 172 """Registers an instance to respond to XML-RPC requests. 173 174 Only one instance can be installed at a time. 175 176 If the registered instance has a _dispatch method then that 177 method will be called with the name of the XML-RPC method and 178 its parameters as a tuple 179 e.g. instance._dispatch('add',(2,3)) 180 181 If the registered instance does not have a _dispatch method 182 then the instance will be searched to find a matching method 183 and, if found, will be called. Methods beginning with an '_' 184 are considered private and will not be called by 185 SimpleXMLRPCServer. 186 187 If a registered function matches an XML-RPC request, then it 188 will be called instead of the registered instance. 189 190 If the optional allow_dotted_names argument is true and the 191 instance does not have a _dispatch method, method names 192 containing dots are supported and resolved, as long as none of 193 the name segments start with an '_'. 194 195 *** SECURITY WARNING: *** 196 197 Enabling the allow_dotted_names options allows intruders 198 to access your module's global variables and may allow 199 intruders to execute arbitrary code on your machine. Only 200 use this option on a secure, closed network. 201 202 """ 203 204 self.instance = instance 205 self.allow_dotted_names = allow_dotted_names 206 207 def register_function(self, function, name=None): 208 """Registers a function to respond to XML-RPC requests. 209 210 The optional name argument can be used to set a Unicode name 211 for the function. 212 """ 213 214 if name is None: 215 name = function.__name__ 216 self.funcs[name] = function 217 218 def register_introspection_functions(self): 219 """Registers the XML-RPC introspection methods in the system 220 namespace. 221 222 see http://xmlrpc.usefulinc.com/doc/reserved.html 223 """ 224 225 self.funcs.update({'system.listMethods' : self.system_listMethods, 226 'system.methodSignature' : self.system_methodSignature, 227 'system.methodHelp' : self.system_methodHelp}) 228 229 def register_multicall_functions(self): 230 """Registers the XML-RPC multicall method in the system 231 namespace. 232 233 see http://www.xmlrpc.com/discuss/msgReader$1208""" 234 235 self.funcs.update({'system.multicall' : self.system_multicall}) 236 237 def _marshaled_dispatch(self, data, dispatch_method = None, path = None): 238 """Dispatches an XML-RPC method from marshalled (XML) data. 239 240 XML-RPC methods are dispatched from the marshalled (XML) data 241 using the _dispatch method and the result is returned as 242 marshalled data. For backwards compatibility, a dispatch 243 function can be provided as an argument (see comment in 244 SimpleXMLRPCRequestHandler.do_POST) but overriding the 245 existing method through subclassing is the preferred means 246 of changing method dispatch behavior. 247 """ 248 249 try: 250 params, method = loads(data, use_builtin_types=self.use_builtin_types) 251 252 # generate response 253 if dispatch_method is not None: 254 response = dispatch_method(method, params) 255 else: 256 response = self._dispatch(method, params) 257 # wrap response in a singleton tuple 258 response = (response,) 259 response = dumps(response, methodresponse=1, 260 allow_none=self.allow_none, encoding=self.encoding) 261 except Fault as fault: 262 response = dumps(fault, allow_none=self.allow_none, 263 encoding=self.encoding) 264 except: 265 # report exception back to server 266 exc_type, exc_value, exc_tb = sys.exc_info() 267 response = dumps( 268 Fault(1, "%s:%s" % (exc_type, exc_value)), 269 encoding=self.encoding, allow_none=self.allow_none, 270 ) 271 272 return response.encode(self.encoding, 'xmlcharrefreplace') 273 274 def system_listMethods(self): 275 """system.listMethods() => ['add', 'subtract', 'multiple'] 276 277 Returns a list of the methods supported by the server.""" 278 279 methods = set(self.funcs.keys()) 280 if self.instance is not None: 281 # Instance can implement _listMethod to return a list of 282 # methods 283 if hasattr(self.instance, '_listMethods'): 284 methods |= set(self.instance._listMethods()) 285 # if the instance has a _dispatch method then we 286 # don't have enough information to provide a list 287 # of methods 288 elif not hasattr(self.instance, '_dispatch'): 289 methods |= set(list_public_methods(self.instance)) 290 return sorted(methods) 291 292 def system_methodSignature(self, method_name): 293 """system.methodSignature('add') => [double, int, int] 294 295 Returns a list describing the signature of the method. In the 296 above example, the add method takes two integers as arguments 297 and returns a double result. 298 299 This server does NOT support system.methodSignature.""" 300 301 # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html 302 303 return 'signatures not supported' 304 305 def system_methodHelp(self, method_name): 306 """system.methodHelp('add') => "Adds two integers together" 307 308 Returns a string containing documentation for the specified method.""" 309 310 method = None 311 if method_name in self.funcs: 312 method = self.funcs[method_name] 313 elif self.instance is not None: 314 # Instance can implement _methodHelp to return help for a method 315 if hasattr(self.instance, '_methodHelp'): 316 return self.instance._methodHelp(method_name) 317 # if the instance has a _dispatch method then we 318 # don't have enough information to provide help 319 elif not hasattr(self.instance, '_dispatch'): 320 try: 321 method = resolve_dotted_attribute( 322 self.instance, 323 method_name, 324 self.allow_dotted_names 325 ) 326 except AttributeError: 327 pass 328 329 # Note that we aren't checking that the method actually 330 # be a callable object of some kind 331 if method is None: 332 return "" 333 else: 334 return pydoc.getdoc(method) 335 336 def system_multicall(self, call_list): 337 """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \ 338[[4], ...] 339 340 Allows the caller to package multiple XML-RPC calls into a single 341 request. 342 343 See http://www.xmlrpc.com/discuss/msgReader$1208 344 """ 345 346 results = [] 347 for call in call_list: 348 method_name = call['methodName'] 349 params = call['params'] 350 351 try: 352 # XXX A marshalling error in any response will fail the entire 353 # multicall. If someone cares they should fix this. 354 results.append([self._dispatch(method_name, params)]) 355 except Fault as fault: 356 results.append( 357 {'faultCode' : fault.faultCode, 358 'faultString' : fault.faultString} 359 ) 360 except: 361 exc_type, exc_value, exc_tb = sys.exc_info() 362 results.append( 363 {'faultCode' : 1, 364 'faultString' : "%s:%s" % (exc_type, exc_value)} 365 ) 366 return results 367 368 def _dispatch(self, method, params): 369 """Dispatches the XML-RPC method. 370 371 XML-RPC calls are forwarded to a registered function that 372 matches the called XML-RPC method name. If no such function 373 exists then the call is forwarded to the registered instance, 374 if available. 375 376 If the registered instance has a _dispatch method then that 377 method will be called with the name of the XML-RPC method and 378 its parameters as a tuple 379 e.g. instance._dispatch('add',(2,3)) 380 381 If the registered instance does not have a _dispatch method 382 then the instance will be searched to find a matching method 383 and, if found, will be called. 384 385 Methods beginning with an '_' are considered private and will 386 not be called. 387 """ 388 389 func = None 390 try: 391 # check to see if a matching function has been registered 392 func = self.funcs[method] 393 except KeyError: 394 if self.instance is not None: 395 # check for a _dispatch method 396 if hasattr(self.instance, '_dispatch'): 397 return self.instance._dispatch(method, params) 398 else: 399 # call instance method directly 400 try: 401 func = resolve_dotted_attribute( 402 self.instance, 403 method, 404 self.allow_dotted_names 405 ) 406 except AttributeError: 407 pass 408 409 if func is not None: 410 return func(*params) 411 else: 412 raise Exception('method "%s" is not supported' % method) 413 414class SimpleXMLRPCRequestHandler(BaseHTTPRequestHandler): 415 """Simple XML-RPC request handler class. 416 417 Handles all HTTP POST requests and attempts to decode them as 418 XML-RPC requests. 419 """ 420 421 # Class attribute listing the accessible path components; 422 # paths not on this list will result in a 404 error. 423 rpc_paths = ('/', '/RPC2') 424 425 #if not None, encode responses larger than this, if possible 426 encode_threshold = 1400 #a common MTU 427 428 #Override form StreamRequestHandler: full buffering of output 429 #and no Nagle. 430 wbufsize = -1 431 disable_nagle_algorithm = True 432 433 # a re to match a gzip Accept-Encoding 434 aepattern = re.compile(r""" 435 \s* ([^\s;]+) \s* #content-coding 436 (;\s* q \s*=\s* ([0-9\.]+))? #q 437 """, re.VERBOSE | re.IGNORECASE) 438 439 def accept_encodings(self): 440 r = {} 441 ae = self.headers.get("Accept-Encoding", "") 442 for e in ae.split(","): 443 match = self.aepattern.match(e) 444 if match: 445 v = match.group(3) 446 v = float(v) if v else 1.0 447 r[match.group(1)] = v 448 return r 449 450 def is_rpc_path_valid(self): 451 if self.rpc_paths: 452 return self.path in self.rpc_paths 453 else: 454 # If .rpc_paths is empty, just assume all paths are legal 455 return True 456 457 def do_POST(self): 458 """Handles the HTTP POST request. 459 460 Attempts to interpret all HTTP POST requests as XML-RPC calls, 461 which are forwarded to the server's _dispatch method for handling. 462 """ 463 464 # Check that the path is legal 465 if not self.is_rpc_path_valid(): 466 self.report_404() 467 return 468 469 try: 470 # Get arguments by reading body of request. 471 # We read this in chunks to avoid straining 472 # socket.read(); around the 10 or 15Mb mark, some platforms 473 # begin to have problems (bug #792570). 474 max_chunk_size = 10*1024*1024 475 size_remaining = int(self.headers["content-length"]) 476 L = [] 477 while size_remaining: 478 chunk_size = min(size_remaining, max_chunk_size) 479 chunk = self.rfile.read(chunk_size) 480 if not chunk: 481 break 482 L.append(chunk) 483 size_remaining -= len(L[-1]) 484 data = b''.join(L) 485 486 data = self.decode_request_content(data) 487 if data is None: 488 return #response has been sent 489 490 # In previous versions of SimpleXMLRPCServer, _dispatch 491 # could be overridden in this class, instead of in 492 # SimpleXMLRPCDispatcher. To maintain backwards compatibility, 493 # check to see if a subclass implements _dispatch and dispatch 494 # using that method if present. 495 response = self.server._marshaled_dispatch( 496 data, getattr(self, '_dispatch', None), self.path 497 ) 498 except Exception as e: # This should only happen if the module is buggy 499 # internal error, report as HTTP server error 500 self.send_response(500) 501 502 # Send information about the exception if requested 503 if hasattr(self.server, '_send_traceback_header') and \ 504 self.server._send_traceback_header: 505 self.send_header("X-exception", str(e)) 506 trace = traceback.format_exc() 507 trace = str(trace.encode('ASCII', 'backslashreplace'), 'ASCII') 508 self.send_header("X-traceback", trace) 509 510 self.send_header("Content-length", "0") 511 self.end_headers() 512 else: 513 self.send_response(200) 514 self.send_header("Content-type", "text/xml") 515 if self.encode_threshold is not None: 516 if len(response) > self.encode_threshold: 517 q = self.accept_encodings().get("gzip", 0) 518 if q: 519 try: 520 response = gzip_encode(response) 521 self.send_header("Content-Encoding", "gzip") 522 except NotImplementedError: 523 pass 524 self.send_header("Content-length", str(len(response))) 525 self.end_headers() 526 self.wfile.write(response) 527 528 def decode_request_content(self, data): 529 #support gzip encoding of request 530 encoding = self.headers.get("content-encoding", "identity").lower() 531 if encoding == "identity": 532 return data 533 if encoding == "gzip": 534 try: 535 return gzip_decode(data) 536 except NotImplementedError: 537 self.send_response(501, "encoding %r not supported" % encoding) 538 except ValueError: 539 self.send_response(400, "error decoding gzip content") 540 else: 541 self.send_response(501, "encoding %r not supported" % encoding) 542 self.send_header("Content-length", "0") 543 self.end_headers() 544 545 def report_404 (self): 546 # Report a 404 error 547 self.send_response(404) 548 response = b'No such page' 549 self.send_header("Content-type", "text/plain") 550 self.send_header("Content-length", str(len(response))) 551 self.end_headers() 552 self.wfile.write(response) 553 554 def log_request(self, code='-', size='-'): 555 """Selectively log an accepted request.""" 556 557 if self.server.logRequests: 558 BaseHTTPRequestHandler.log_request(self, code, size) 559 560class SimpleXMLRPCServer(socketserver.TCPServer, 561 SimpleXMLRPCDispatcher): 562 """Simple XML-RPC server. 563 564 Simple XML-RPC server that allows functions and a single instance 565 to be installed to handle requests. The default implementation 566 attempts to dispatch XML-RPC calls to the functions or instance 567 installed in the server. Override the _dispatch method inherited 568 from SimpleXMLRPCDispatcher to change this behavior. 569 """ 570 571 allow_reuse_address = True 572 573 # Warning: this is for debugging purposes only! Never set this to True in 574 # production code, as will be sending out sensitive information (exception 575 # and stack trace details) when exceptions are raised inside 576 # SimpleXMLRPCRequestHandler.do_POST 577 _send_traceback_header = False 578 579 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, 580 logRequests=True, allow_none=False, encoding=None, 581 bind_and_activate=True, use_builtin_types=False): 582 self.logRequests = logRequests 583 584 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types) 585 socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate) 586 587 588class MultiPathXMLRPCServer(SimpleXMLRPCServer): 589 """Multipath XML-RPC Server 590 This specialization of SimpleXMLRPCServer allows the user to create 591 multiple Dispatcher instances and assign them to different 592 HTTP request paths. This makes it possible to run two or more 593 'virtual XML-RPC servers' at the same port. 594 Make sure that the requestHandler accepts the paths in question. 595 """ 596 def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, 597 logRequests=True, allow_none=False, encoding=None, 598 bind_and_activate=True, use_builtin_types=False): 599 600 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none, 601 encoding, bind_and_activate, use_builtin_types) 602 self.dispatchers = {} 603 self.allow_none = allow_none 604 self.encoding = encoding or 'utf-8' 605 606 def add_dispatcher(self, path, dispatcher): 607 self.dispatchers[path] = dispatcher 608 return dispatcher 609 610 def get_dispatcher(self, path): 611 return self.dispatchers[path] 612 613 def _marshaled_dispatch(self, data, dispatch_method = None, path = None): 614 try: 615 response = self.dispatchers[path]._marshaled_dispatch( 616 data, dispatch_method, path) 617 except: 618 # report low level exception back to server 619 # (each dispatcher should have handled their own 620 # exceptions) 621 exc_type, exc_value = sys.exc_info()[:2] 622 response = dumps( 623 Fault(1, "%s:%s" % (exc_type, exc_value)), 624 encoding=self.encoding, allow_none=self.allow_none) 625 response = response.encode(self.encoding, 'xmlcharrefreplace') 626 return response 627 628class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): 629 """Simple handler for XML-RPC data passed through CGI.""" 630 631 def __init__(self, allow_none=False, encoding=None, use_builtin_types=False): 632 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types) 633 634 def handle_xmlrpc(self, request_text): 635 """Handle a single XML-RPC request""" 636 637 response = self._marshaled_dispatch(request_text) 638 639 print('Content-Type: text/xml') 640 print('Content-Length: %d' % len(response)) 641 print() 642 sys.stdout.flush() 643 sys.stdout.buffer.write(response) 644 sys.stdout.buffer.flush() 645 646 def handle_get(self): 647 """Handle a single HTTP GET request. 648 649 Default implementation indicates an error because 650 XML-RPC uses the POST method. 651 """ 652 653 code = 400 654 message, explain = BaseHTTPRequestHandler.responses[code] 655 656 response = http.server.DEFAULT_ERROR_MESSAGE % \ 657 { 658 'code' : code, 659 'message' : message, 660 'explain' : explain 661 } 662 response = response.encode('utf-8') 663 print('Status: %d %s' % (code, message)) 664 print('Content-Type: %s' % http.server.DEFAULT_ERROR_CONTENT_TYPE) 665 print('Content-Length: %d' % len(response)) 666 print() 667 sys.stdout.flush() 668 sys.stdout.buffer.write(response) 669 sys.stdout.buffer.flush() 670 671 def handle_request(self, request_text=None): 672 """Handle a single XML-RPC request passed through a CGI post method. 673 674 If no XML data is given then it is read from stdin. The resulting 675 XML-RPC response is printed to stdout along with the correct HTTP 676 headers. 677 """ 678 679 if request_text is None and \ 680 os.environ.get('REQUEST_METHOD', None) == 'GET': 681 self.handle_get() 682 else: 683 # POST data is normally available through stdin 684 try: 685 length = int(os.environ.get('CONTENT_LENGTH', None)) 686 except (ValueError, TypeError): 687 length = -1 688 if request_text is None: 689 request_text = sys.stdin.read(length) 690 691 self.handle_xmlrpc(request_text) 692 693 694# ----------------------------------------------------------------------------- 695# Self documenting XML-RPC Server. 696 697class ServerHTMLDoc(pydoc.HTMLDoc): 698 """Class used to generate pydoc HTML document for a server""" 699 700 def markup(self, text, escape=None, funcs={}, classes={}, methods={}): 701 """Mark up some plain text, given a context of symbols to look for. 702 Each context dictionary maps object names to anchor names.""" 703 escape = escape or self.escape 704 results = [] 705 here = 0 706 707 # XXX Note that this regular expression does not allow for the 708 # hyperlinking of arbitrary strings being used as method 709 # names. Only methods with names consisting of word characters 710 # and '.'s are hyperlinked. 711 pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|' 712 r'RFC[- ]?(\d+)|' 713 r'PEP[- ]?(\d+)|' 714 r'(self\.)?((?:\w|\.)+))\b') 715 while 1: 716 match = pattern.search(text, here) 717 if not match: break 718 start, end = match.span() 719 results.append(escape(text[here:start])) 720 721 all, scheme, rfc, pep, selfdot, name = match.groups() 722 if scheme: 723 url = escape(all).replace('"', '"') 724 results.append('<a href="%s">%s</a>' % (url, url)) 725 elif rfc: 726 url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc) 727 results.append('<a href="%s">%s</a>' % (url, escape(all))) 728 elif pep: 729 url = 'http://www.python.org/dev/peps/pep-%04d/' % int(pep) 730 results.append('<a href="%s">%s</a>' % (url, escape(all))) 731 elif text[end:end+1] == '(': 732 results.append(self.namelink(name, methods, funcs, classes)) 733 elif selfdot: 734 results.append('self.<strong>%s</strong>' % name) 735 else: 736 results.append(self.namelink(name, classes)) 737 here = end 738 results.append(escape(text[here:])) 739 return ''.join(results) 740 741 def docroutine(self, object, name, mod=None, 742 funcs={}, classes={}, methods={}, cl=None): 743 """Produce HTML documentation for a function or method object.""" 744 745 anchor = (cl and cl.__name__ or '') + '-' + name 746 note = '' 747 748 title = '<a name="%s"><strong>%s</strong></a>' % ( 749 self.escape(anchor), self.escape(name)) 750 751 if inspect.ismethod(object): 752 args = inspect.getfullargspec(object) 753 # exclude the argument bound to the instance, it will be 754 # confusing to the non-Python user 755 argspec = inspect.formatargspec ( 756 args.args[1:], 757 args.varargs, 758 args.varkw, 759 args.defaults, 760 annotations=args.annotations, 761 formatvalue=self.formatvalue 762 ) 763 elif inspect.isfunction(object): 764 args = inspect.getfullargspec(object) 765 argspec = inspect.formatargspec( 766 args.args, args.varargs, args.varkw, args.defaults, 767 annotations=args.annotations, 768 formatvalue=self.formatvalue) 769 else: 770 argspec = '(...)' 771 772 if isinstance(object, tuple): 773 argspec = object[0] or argspec 774 docstring = object[1] or "" 775 else: 776 docstring = pydoc.getdoc(object) 777 778 decl = title + argspec + (note and self.grey( 779 '<font face="helvetica, arial">%s</font>' % note)) 780 781 doc = self.markup( 782 docstring, self.preformat, funcs, classes, methods) 783 doc = doc and '<dd><tt>%s</tt></dd>' % doc 784 return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc) 785 786 def docserver(self, server_name, package_documentation, methods): 787 """Produce HTML documentation for an XML-RPC server.""" 788 789 fdict = {} 790 for key, value in methods.items(): 791 fdict[key] = '#-' + key 792 fdict[value] = fdict[key] 793 794 server_name = self.escape(server_name) 795 head = '<big><big><strong>%s</strong></big></big>' % server_name 796 result = self.heading(head, '#ffffff', '#7799ee') 797 798 doc = self.markup(package_documentation, self.preformat, fdict) 799 doc = doc and '<tt>%s</tt>' % doc 800 result = result + '<p>%s</p>\n' % doc 801 802 contents = [] 803 method_items = sorted(methods.items()) 804 for key, value in method_items: 805 contents.append(self.docroutine(value, key, funcs=fdict)) 806 result = result + self.bigsection( 807 'Methods', '#ffffff', '#eeaa77', ''.join(contents)) 808 809 return result 810 811class XMLRPCDocGenerator: 812 """Generates documentation for an XML-RPC server. 813 814 This class is designed as mix-in and should not 815 be constructed directly. 816 """ 817 818 def __init__(self): 819 # setup variables used for HTML documentation 820 self.server_name = 'XML-RPC Server Documentation' 821 self.server_documentation = \ 822 "This server exports the following methods through the XML-RPC "\ 823 "protocol." 824 self.server_title = 'XML-RPC Server Documentation' 825 826 def set_server_title(self, server_title): 827 """Set the HTML title of the generated server documentation""" 828 829 self.server_title = server_title 830 831 def set_server_name(self, server_name): 832 """Set the name of the generated HTML server documentation""" 833 834 self.server_name = server_name 835 836 def set_server_documentation(self, server_documentation): 837 """Set the documentation string for the entire server.""" 838 839 self.server_documentation = server_documentation 840 841 def generate_html_documentation(self): 842 """generate_html_documentation() => html documentation for the server 843 844 Generates HTML documentation for the server using introspection for 845 installed functions and instances that do not implement the 846 _dispatch method. Alternatively, instances can choose to implement 847 the _get_method_argstring(method_name) method to provide the 848 argument string used in the documentation and the 849 _methodHelp(method_name) method to provide the help text used 850 in the documentation.""" 851 852 methods = {} 853 854 for method_name in self.system_listMethods(): 855 if method_name in self.funcs: 856 method = self.funcs[method_name] 857 elif self.instance is not None: 858 method_info = [None, None] # argspec, documentation 859 if hasattr(self.instance, '_get_method_argstring'): 860 method_info[0] = self.instance._get_method_argstring(method_name) 861 if hasattr(self.instance, '_methodHelp'): 862 method_info[1] = self.instance._methodHelp(method_name) 863 864 method_info = tuple(method_info) 865 if method_info != (None, None): 866 method = method_info 867 elif not hasattr(self.instance, '_dispatch'): 868 try: 869 method = resolve_dotted_attribute( 870 self.instance, 871 method_name 872 ) 873 except AttributeError: 874 method = method_info 875 else: 876 method = method_info 877 else: 878 assert 0, "Could not find method in self.functions and no "\ 879 "instance installed" 880 881 methods[method_name] = method 882 883 documenter = ServerHTMLDoc() 884 documentation = documenter.docserver( 885 self.server_name, 886 self.server_documentation, 887 methods 888 ) 889 890 return documenter.page(self.server_title, documentation) 891 892class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): 893 """XML-RPC and documentation request handler class. 894 895 Handles all HTTP POST requests and attempts to decode them as 896 XML-RPC requests. 897 898 Handles all HTTP GET requests and interprets them as requests 899 for documentation. 900 """ 901 902 def do_GET(self): 903 """Handles the HTTP GET request. 904 905 Interpret all HTTP GET requests as requests for server 906 documentation. 907 """ 908 # Check that the path is legal 909 if not self.is_rpc_path_valid(): 910 self.report_404() 911 return 912 913 response = self.server.generate_html_documentation().encode('utf-8') 914 self.send_response(200) 915 self.send_header("Content-type", "text/html") 916 self.send_header("Content-length", str(len(response))) 917 self.end_headers() 918 self.wfile.write(response) 919 920class DocXMLRPCServer( SimpleXMLRPCServer, 921 XMLRPCDocGenerator): 922 """XML-RPC and HTML documentation server. 923 924 Adds the ability to serve server documentation to the capabilities 925 of SimpleXMLRPCServer. 926 """ 927 928 def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler, 929 logRequests=True, allow_none=False, encoding=None, 930 bind_and_activate=True, use_builtin_types=False): 931 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, 932 allow_none, encoding, bind_and_activate, 933 use_builtin_types) 934 XMLRPCDocGenerator.__init__(self) 935 936class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler, 937 XMLRPCDocGenerator): 938 """Handler for XML-RPC data and documentation requests passed through 939 CGI""" 940 941 def handle_get(self): 942 """Handles the HTTP GET request. 943 944 Interpret all HTTP GET requests as requests for server 945 documentation. 946 """ 947 948 response = self.generate_html_documentation().encode('utf-8') 949 950 print('Content-Type: text/html') 951 print('Content-Length: %d' % len(response)) 952 print() 953 sys.stdout.flush() 954 sys.stdout.buffer.write(response) 955 sys.stdout.buffer.flush() 956 957 def __init__(self): 958 CGIXMLRPCRequestHandler.__init__(self) 959 XMLRPCDocGenerator.__init__(self) 960 961 962if __name__ == '__main__': 963 import datetime 964 965 class ExampleService: 966 def getData(self): 967 return '42' 968 969 class currentTime: 970 @staticmethod 971 def getCurrentTime(): 972 return datetime.datetime.now() 973 974 with SimpleXMLRPCServer(("localhost", 8000)) as server: 975 server.register_function(pow) 976 server.register_function(lambda x,y: x+y, 'add') 977 server.register_instance(ExampleService(), allow_dotted_names=True) 978 server.register_multicall_functions() 979 print('Serving XML-RPC on localhost port 8000') 980 print('It is advisable to run this example server within a secure, closed network.') 981 try: 982 server.serve_forever() 983 except KeyboardInterrupt: 984 print("\nKeyboard interrupt received, exiting.") 985 sys.exit(0) 986