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