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