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