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