python (3.12.0)
1 r"""XML-RPC Servers.
2
3 This module can be used to create simple XML-RPC servers
4 by creating a server and either installing functions, a
5 class instance, or by extending the SimpleXMLRPCServer
6 class.
7
8 It can also be used to handle XML-RPC requests in a CGI
9 environment using CGIXMLRPCRequestHandler.
10
11 The Doc* classes can be used to create XML-RPC servers that
12 serve pydoc-style documentation in response to HTTP
13 GET requests. This documentation is dynamically generated
14 based on the functions and methods registered with the
15 server.
16
17 A list of possible usage patterns follows:
18
19 1. Install functions:
20
21 server = SimpleXMLRPCServer(("localhost", 8000))
22 server.register_function(pow)
23 server.register_function(lambda x,y: x+y, 'add')
24 server.serve_forever()
25
26 2. Install an instance:
27
28 class 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
41 server = SimpleXMLRPCServer(("localhost", 8000))
42 server.register_introspection_functions()
43 server.register_instance(MyFuncs())
44 server.serve_forever()
45
46 3. Install an instance with custom dispatch method:
47
48 class 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
72 server = SimpleXMLRPCServer(("localhost", 8000))
73 server.register_introspection_functions()
74 server.register_instance(Math())
75 server.serve_forever()
76
77 4. Subclass SimpleXMLRPCServer:
78
79 class 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
94 server = MathServer(("localhost", 8000))
95 server.serve_forever()
96
97 5. CGI script:
98
99 server = CGIXMLRPCRequestHandler()
100 server.register_function(pow)
101 server.handle_request()
102 """
103
104 # Written by Brian Quinlan (brian@sweetapp.com).
105 # Based on code written by Fredrik Lundh.
106
107 from xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode
108 from http.server import BaseHTTPRequestHandler
109 from functools import partial
110 from inspect import signature
111 import html
112 import http.server
113 import socketserver
114 import sys
115 import os
116 import re
117 import pydoc
118 import traceback
119 try:
120 import fcntl
121 except ImportError:
122 fcntl = None
123
124 def 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
148 def 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
156 class ESC[4;38;5;81mSimpleXMLRPCDispatcher:
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
423 class ESC[4;38;5;81mSimpleXMLRPCRequestHandler(ESC[4;38;5;149mBaseHTTPRequestHandler):
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
569 class ESC[4;38;5;81mSimpleXMLRPCServer(ESC[4;38;5;149msocketserverESC[4;38;5;149m.ESC[4;38;5;149mTCPServer,
570 ESC[4;38;5;149mSimpleXMLRPCDispatcher):
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
597 class ESC[4;38;5;81mMultiPathXMLRPCServer(ESC[4;38;5;149mSimpleXMLRPCServer):
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
636 class ESC[4;38;5;81mCGIXMLRPCRequestHandler(ESC[4;38;5;149mSimpleXMLRPCDispatcher):
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
705 class ESC[4;38;5;81mServerHTMLDoc(ESC[4;38;5;149mpydocESC[4;38;5;149m.ESC[4;38;5;149mHTMLDoc):
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 match := pattern.search(text, here):
724 start, end = match.span()
725 results.append(escape(text[here:start]))
726
727 all, scheme, rfc, pep, selfdot, name = match.groups()
728 if scheme:
729 url = escape(all).replace('"', '"')
730 results.append('<a href="%s">%s</a>' % (url, url))
731 elif rfc:
732 url = 'https://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc)
733 results.append('<a href="%s">%s</a>' % (url, escape(all)))
734 elif pep:
735 url = 'https://peps.python.org/pep-%04d/' % int(pep)
736 results.append('<a href="%s">%s</a>' % (url, escape(all)))
737 elif text[end:end+1] == '(':
738 results.append(self.namelink(name, methods, funcs, classes))
739 elif selfdot:
740 results.append('self.<strong>%s</strong>' % name)
741 else:
742 results.append(self.namelink(name, classes))
743 here = end
744 results.append(escape(text[here:]))
745 return ''.join(results)
746
747 def docroutine(self, object, name, mod=None,
748 funcs={}, classes={}, methods={}, cl=None):
749 """Produce HTML documentation for a function or method object."""
750
751 anchor = (cl and cl.__name__ or '') + '-' + name
752 note = ''
753
754 title = '<a name="%s"><strong>%s</strong></a>' % (
755 self.escape(anchor), self.escape(name))
756
757 if callable(object):
758 argspec = str(signature(object))
759 else:
760 argspec = '(...)'
761
762 if isinstance(object, tuple):
763 argspec = object[0] or argspec
764 docstring = object[1] or ""
765 else:
766 docstring = pydoc.getdoc(object)
767
768 decl = title + argspec + (note and self.grey(
769 '<font face="helvetica, arial">%s</font>' % note))
770
771 doc = self.markup(
772 docstring, self.preformat, funcs, classes, methods)
773 doc = doc and '<dd><tt>%s</tt></dd>' % doc
774 return '<dl><dt>%s</dt>%s</dl>\n' % (decl, doc)
775
776 def docserver(self, server_name, package_documentation, methods):
777 """Produce HTML documentation for an XML-RPC server."""
778
779 fdict = {}
780 for key, value in methods.items():
781 fdict[key] = '#-' + key
782 fdict[value] = fdict[key]
783
784 server_name = self.escape(server_name)
785 head = '<big><big><strong>%s</strong></big></big>' % server_name
786 result = self.heading(head)
787
788 doc = self.markup(package_documentation, self.preformat, fdict)
789 doc = doc and '<tt>%s</tt>' % doc
790 result = result + '<p>%s</p>\n' % doc
791
792 contents = []
793 method_items = sorted(methods.items())
794 for key, value in method_items:
795 contents.append(self.docroutine(value, key, funcs=fdict))
796 result = result + self.bigsection(
797 'Methods', 'functions', ''.join(contents))
798
799 return result
800
801
802 def page(self, title, contents):
803 """Format an HTML page."""
804 css_path = "/pydoc.css"
805 css_link = (
806 '<link rel="stylesheet" type="text/css" href="%s">' %
807 css_path)
808 return '''\
809 <!DOCTYPE>
810 <html lang="en">
811 <head>
812 <meta charset="utf-8">
813 <title>Python: %s</title>
814 %s</head><body>%s</body></html>''' % (title, css_link, contents)
815
816 class ESC[4;38;5;81mXMLRPCDocGenerator:
817 """Generates documentation for an XML-RPC server.
818
819 This class is designed as mix-in and should not
820 be constructed directly.
821 """
822
823 def __init__(self):
824 # setup variables used for HTML documentation
825 self.server_name = 'XML-RPC Server Documentation'
826 self.server_documentation = \
827 "This server exports the following methods through the XML-RPC "\
828 "protocol."
829 self.server_title = 'XML-RPC Server Documentation'
830
831 def set_server_title(self, server_title):
832 """Set the HTML title of the generated server documentation"""
833
834 self.server_title = server_title
835
836 def set_server_name(self, server_name):
837 """Set the name of the generated HTML server documentation"""
838
839 self.server_name = server_name
840
841 def set_server_documentation(self, server_documentation):
842 """Set the documentation string for the entire server."""
843
844 self.server_documentation = server_documentation
845
846 def generate_html_documentation(self):
847 """generate_html_documentation() => html documentation for the server
848
849 Generates HTML documentation for the server using introspection for
850 installed functions and instances that do not implement the
851 _dispatch method. Alternatively, instances can choose to implement
852 the _get_method_argstring(method_name) method to provide the
853 argument string used in the documentation and the
854 _methodHelp(method_name) method to provide the help text used
855 in the documentation."""
856
857 methods = {}
858
859 for method_name in self.system_listMethods():
860 if method_name in self.funcs:
861 method = self.funcs[method_name]
862 elif self.instance is not None:
863 method_info = [None, None] # argspec, documentation
864 if hasattr(self.instance, '_get_method_argstring'):
865 method_info[0] = self.instance._get_method_argstring(method_name)
866 if hasattr(self.instance, '_methodHelp'):
867 method_info[1] = self.instance._methodHelp(method_name)
868
869 method_info = tuple(method_info)
870 if method_info != (None, None):
871 method = method_info
872 elif not hasattr(self.instance, '_dispatch'):
873 try:
874 method = resolve_dotted_attribute(
875 self.instance,
876 method_name
877 )
878 except AttributeError:
879 method = method_info
880 else:
881 method = method_info
882 else:
883 assert 0, "Could not find method in self.functions and no "\
884 "instance installed"
885
886 methods[method_name] = method
887
888 documenter = ServerHTMLDoc()
889 documentation = documenter.docserver(
890 self.server_name,
891 self.server_documentation,
892 methods
893 )
894
895 return documenter.page(html.escape(self.server_title), documentation)
896
897 class ESC[4;38;5;81mDocXMLRPCRequestHandler(ESC[4;38;5;149mSimpleXMLRPCRequestHandler):
898 """XML-RPC and documentation request handler class.
899
900 Handles all HTTP POST requests and attempts to decode them as
901 XML-RPC requests.
902
903 Handles all HTTP GET requests and interprets them as requests
904 for documentation.
905 """
906
907 def _get_css(self, url):
908 path_here = os.path.dirname(os.path.realpath(__file__))
909 css_path = os.path.join(path_here, "..", "pydoc_data", "_pydoc.css")
910 with open(css_path, mode="rb") as fp:
911 return fp.read()
912
913 def do_GET(self):
914 """Handles the HTTP GET request.
915
916 Interpret all HTTP GET requests as requests for server
917 documentation.
918 """
919 # Check that the path is legal
920 if not self.is_rpc_path_valid():
921 self.report_404()
922 return
923
924 if self.path.endswith('.css'):
925 content_type = 'text/css'
926 response = self._get_css(self.path)
927 else:
928 content_type = 'text/html'
929 response = self.server.generate_html_documentation().encode('utf-8')
930
931 self.send_response(200)
932 self.send_header('Content-Type', '%s; charset=UTF-8' % content_type)
933 self.send_header("Content-length", str(len(response)))
934 self.end_headers()
935 self.wfile.write(response)
936
937 class ESC[4;38;5;81mDocXMLRPCServer( ESC[4;38;5;149mSimpleXMLRPCServer,
938 ESC[4;38;5;149mXMLRPCDocGenerator):
939 """XML-RPC and HTML documentation server.
940
941 Adds the ability to serve server documentation to the capabilities
942 of SimpleXMLRPCServer.
943 """
944
945 def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler,
946 logRequests=True, allow_none=False, encoding=None,
947 bind_and_activate=True, use_builtin_types=False):
948 SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests,
949 allow_none, encoding, bind_and_activate,
950 use_builtin_types)
951 XMLRPCDocGenerator.__init__(self)
952
953 class ESC[4;38;5;81mDocCGIXMLRPCRequestHandler( ESC[4;38;5;149mCGIXMLRPCRequestHandler,
954 ESC[4;38;5;149mXMLRPCDocGenerator):
955 """Handler for XML-RPC data and documentation requests passed through
956 CGI"""
957
958 def handle_get(self):
959 """Handles the HTTP GET request.
960
961 Interpret all HTTP GET requests as requests for server
962 documentation.
963 """
964
965 response = self.generate_html_documentation().encode('utf-8')
966
967 print('Content-Type: text/html')
968 print('Content-Length: %d' % len(response))
969 print()
970 sys.stdout.flush()
971 sys.stdout.buffer.write(response)
972 sys.stdout.buffer.flush()
973
974 def __init__(self):
975 CGIXMLRPCRequestHandler.__init__(self)
976 XMLRPCDocGenerator.__init__(self)
977
978
979 if __name__ == '__main__':
980 import datetime
981
982 class ESC[4;38;5;81mExampleService:
983 def getData(self):
984 return '42'
985
986 class ESC[4;38;5;81mcurrentTime:
987 @staticmethod
988 def getCurrentTime():
989 return datetime.datetime.now()
990
991 with SimpleXMLRPCServer(("localhost", 8000)) as server:
992 server.register_function(pow)
993 server.register_function(lambda x,y: x+y, 'add')
994 server.register_instance(ExampleService(), allow_dotted_names=True)
995 server.register_multicall_functions()
996 print('Serving XML-RPC on localhost port 8000')
997 print('It is advisable to run this example server within a secure, closed network.')
998 try:
999 server.serve_forever()
1000 except KeyboardInterrupt:
1001 print("\nKeyboard interrupt received, exiting.")
1002 sys.exit(0)