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