| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.web.test.test_xmlrpc -*- | |
| 2 # | |
| 3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
| 4 # See LICENSE for details. | |
| 5 | |
| 6 | |
| 7 """ | |
| 8 A generic resource for publishing objects via XML-RPC. | |
| 9 | |
| 10 Requires xmlrpclib (comes standard with Python 2.2 and later, otherwise can be | |
| 11 downloaded from http://www.pythonware.com/products/xmlrpc/). | |
| 12 | |
| 13 Maintainer: U{Itamar Shtull-Trauring<mailto:twisted@itamarst.org>} | |
| 14 """ | |
| 15 from __future__ import nested_scopes | |
| 16 | |
| 17 __version__ = "$Revision: 1.32 $"[11:-2] | |
| 18 | |
| 19 # System Imports | |
| 20 import xmlrpclib | |
| 21 import urlparse | |
| 22 | |
| 23 # Sibling Imports | |
| 24 from twisted.web import resource, server, http | |
| 25 from twisted.internet import defer, protocol, reactor | |
| 26 from twisted.python import log, reflect, failure | |
| 27 | |
| 28 # These are deprecated, use the class level definitions | |
| 29 NOT_FOUND = 8001 | |
| 30 FAILURE = 8002 | |
| 31 | |
| 32 | |
| 33 # Useful so people don't need to import xmlrpclib directly | |
| 34 Fault = xmlrpclib.Fault | |
| 35 Binary = xmlrpclib.Binary | |
| 36 Boolean = xmlrpclib.Boolean | |
| 37 DateTime = xmlrpclib.DateTime | |
| 38 | |
| 39 class NoSuchFunction(Fault): | |
| 40 """ | |
| 41 There is no function by the given name. | |
| 42 """ | |
| 43 | |
| 44 | |
| 45 class Handler: | |
| 46 """ | |
| 47 Handle a XML-RPC request and store the state for a request in progress. | |
| 48 | |
| 49 Override the run() method and return result using self.result, | |
| 50 a Deferred. | |
| 51 | |
| 52 We require this class since we're not using threads, so we can't | |
| 53 encapsulate state in a running function if we're going to have | |
| 54 to wait for results. | |
| 55 | |
| 56 For example, lets say we want to authenticate against twisted.cred, | |
| 57 run a LDAP query and then pass its result to a database query, all | |
| 58 as a result of a single XML-RPC command. We'd use a Handler instance | |
| 59 to store the state of the running command. | |
| 60 """ | |
| 61 | |
| 62 def __init__(self, resource, *args): | |
| 63 self.resource = resource # the XML-RPC resource we are connected to | |
| 64 self.result = defer.Deferred() | |
| 65 self.run(*args) | |
| 66 | |
| 67 def run(self, *args): | |
| 68 # event driven equivalent of 'raise UnimplementedError' | |
| 69 self.result.errback( | |
| 70 NotImplementedError("Implement run() in subclasses")) | |
| 71 | |
| 72 | |
| 73 class XMLRPC(resource.Resource): | |
| 74 """ | |
| 75 A resource that implements XML-RPC. | |
| 76 | |
| 77 You probably want to connect this to '/RPC2'. | |
| 78 | |
| 79 Methods published can return XML-RPC serializable results, Faults, | |
| 80 Binary, Boolean, DateTime, Deferreds, or Handler instances. | |
| 81 | |
| 82 By default methods beginning with 'xmlrpc_' are published. | |
| 83 | |
| 84 Sub-handlers for prefixed methods (e.g., system.listMethods) | |
| 85 can be added with putSubHandler. By default, prefixes are | |
| 86 separated with a '.'. Override self.separator to change this. | |
| 87 """ | |
| 88 | |
| 89 # Error codes for Twisted, if they conflict with yours then | |
| 90 # modify them at runtime. | |
| 91 NOT_FOUND = 8001 | |
| 92 FAILURE = 8002 | |
| 93 | |
| 94 isLeaf = 1 | |
| 95 separator = '.' | |
| 96 allowedMethods = ('POST',) | |
| 97 | |
| 98 def __init__(self, allowNone=False): | |
| 99 resource.Resource.__init__(self) | |
| 100 self.subHandlers = {} | |
| 101 self.allowNone = allowNone | |
| 102 | |
| 103 def putSubHandler(self, prefix, handler): | |
| 104 self.subHandlers[prefix] = handler | |
| 105 | |
| 106 def getSubHandler(self, prefix): | |
| 107 return self.subHandlers.get(prefix, None) | |
| 108 | |
| 109 def getSubHandlerPrefixes(self): | |
| 110 return self.subHandlers.keys() | |
| 111 | |
| 112 def render_POST(self, request): | |
| 113 request.content.seek(0, 0) | |
| 114 request.setHeader("content-type", "text/xml") | |
| 115 try: | |
| 116 args, functionPath = xmlrpclib.loads(request.content.read()) | |
| 117 except Exception, e: | |
| 118 f = Fault(self.FAILURE, "Can't deserialize input: %s" % (e,)) | |
| 119 self._cbRender(f, request) | |
| 120 else: | |
| 121 try: | |
| 122 function = self._getFunction(functionPath) | |
| 123 except Fault, f: | |
| 124 self._cbRender(f, request) | |
| 125 else: | |
| 126 defer.maybeDeferred(function, *args).addErrback( | |
| 127 self._ebRender | |
| 128 ).addCallback( | |
| 129 self._cbRender, request | |
| 130 ) | |
| 131 return server.NOT_DONE_YET | |
| 132 | |
| 133 def _cbRender(self, result, request): | |
| 134 if isinstance(result, Handler): | |
| 135 result = result.result | |
| 136 if not isinstance(result, Fault): | |
| 137 result = (result,) | |
| 138 try: | |
| 139 s = xmlrpclib.dumps(result, methodresponse=True, | |
| 140 allow_none=self.allowNone) | |
| 141 except Exception, e: | |
| 142 f = Fault(self.FAILURE, "Can't serialize output: %s" % (e,)) | |
| 143 s = xmlrpclib.dumps(f, methodresponse=True, | |
| 144 allow_none=self.allowNone) | |
| 145 request.setHeader("content-length", str(len(s))) | |
| 146 request.write(s) | |
| 147 request.finish() | |
| 148 | |
| 149 def _ebRender(self, failure): | |
| 150 if isinstance(failure.value, Fault): | |
| 151 return failure.value | |
| 152 log.err(failure) | |
| 153 return Fault(self.FAILURE, "error") | |
| 154 | |
| 155 def _getFunction(self, functionPath): | |
| 156 """ | |
| 157 Given a string, return a function, or raise NoSuchFunction. | |
| 158 | |
| 159 This returned function will be called, and should return the result | |
| 160 of the call, a Deferred, or a Fault instance. | |
| 161 | |
| 162 Override in subclasses if you want your own policy. The default | |
| 163 policy is that given functionPath 'foo', return the method at | |
| 164 self.xmlrpc_foo, i.e. getattr(self, "xmlrpc_" + functionPath). | |
| 165 If functionPath contains self.separator, the sub-handler for | |
| 166 the initial prefix is used to search for the remaining path. | |
| 167 """ | |
| 168 if functionPath.find(self.separator) != -1: | |
| 169 prefix, functionPath = functionPath.split(self.separator, 1) | |
| 170 handler = self.getSubHandler(prefix) | |
| 171 if handler is None: | |
| 172 raise NoSuchFunction(self.NOT_FOUND, | |
| 173 "no such subHandler %s" % prefix) | |
| 174 return handler._getFunction(functionPath) | |
| 175 | |
| 176 f = getattr(self, "xmlrpc_%s" % functionPath, None) | |
| 177 if not f: | |
| 178 raise NoSuchFunction(self.NOT_FOUND, | |
| 179 "function %s not found" % functionPath) | |
| 180 elif not callable(f): | |
| 181 raise NoSuchFunction(self.NOT_FOUND, | |
| 182 "function %s not callable" % functionPath) | |
| 183 else: | |
| 184 return f | |
| 185 | |
| 186 def _listFunctions(self): | |
| 187 """ | |
| 188 Return a list of the names of all xmlrpc methods. | |
| 189 """ | |
| 190 return reflect.prefixedMethodNames(self.__class__, 'xmlrpc_') | |
| 191 | |
| 192 | |
| 193 class XMLRPCIntrospection(XMLRPC): | |
| 194 """ | |
| 195 Implement the XML-RPC Introspection API. | |
| 196 | |
| 197 By default, the methodHelp method returns the 'help' method attribute, | |
| 198 if it exists, otherwise the __doc__ method attribute, if it exists, | |
| 199 otherwise the empty string. | |
| 200 | |
| 201 To enable the methodSignature method, add a 'signature' method attribute | |
| 202 containing a list of lists. See methodSignature's documentation for the | |
| 203 format. Note the type strings should be XML-RPC types, not Python types. | |
| 204 """ | |
| 205 | |
| 206 def __init__(self, parent): | |
| 207 """ | |
| 208 Implement Introspection support for an XMLRPC server. | |
| 209 | |
| 210 @param parent: the XMLRPC server to add Introspection support to. | |
| 211 """ | |
| 212 | |
| 213 XMLRPC.__init__(self) | |
| 214 self._xmlrpc_parent = parent | |
| 215 | |
| 216 def xmlrpc_listMethods(self): | |
| 217 """ | |
| 218 Return a list of the method names implemented by this server. | |
| 219 """ | |
| 220 functions = [] | |
| 221 todo = [(self._xmlrpc_parent, '')] | |
| 222 while todo: | |
| 223 obj, prefix = todo.pop(0) | |
| 224 functions.extend([prefix + name for name in obj._listFunctions()]) | |
| 225 todo.extend([ (obj.getSubHandler(name), | |
| 226 prefix + name + obj.separator) | |
| 227 for name in obj.getSubHandlerPrefixes() ]) | |
| 228 return functions | |
| 229 | |
| 230 xmlrpc_listMethods.signature = [['array']] | |
| 231 | |
| 232 def xmlrpc_methodHelp(self, method): | |
| 233 """ | |
| 234 Return a documentation string describing the use of the given method. | |
| 235 """ | |
| 236 method = self._xmlrpc_parent._getFunction(method) | |
| 237 return (getattr(method, 'help', None) | |
| 238 or getattr(method, '__doc__', None) or '') | |
| 239 | |
| 240 xmlrpc_methodHelp.signature = [['string', 'string']] | |
| 241 | |
| 242 def xmlrpc_methodSignature(self, method): | |
| 243 """ | |
| 244 Return a list of type signatures. | |
| 245 | |
| 246 Each type signature is a list of the form [rtype, type1, type2, ...] | |
| 247 where rtype is the return type and typeN is the type of the Nth | |
| 248 argument. If no signature information is available, the empty | |
| 249 string is returned. | |
| 250 """ | |
| 251 method = self._xmlrpc_parent._getFunction(method) | |
| 252 return getattr(method, 'signature', None) or '' | |
| 253 | |
| 254 xmlrpc_methodSignature.signature = [['array', 'string'], | |
| 255 ['string', 'string']] | |
| 256 | |
| 257 | |
| 258 def addIntrospection(xmlrpc): | |
| 259 """ | |
| 260 Add Introspection support to an XMLRPC server. | |
| 261 | |
| 262 @param xmlrpc: The xmlrpc server to add Introspection support to. | |
| 263 """ | |
| 264 xmlrpc.putSubHandler('system', XMLRPCIntrospection(xmlrpc)) | |
| 265 | |
| 266 | |
| 267 class QueryProtocol(http.HTTPClient): | |
| 268 | |
| 269 def connectionMade(self): | |
| 270 self.sendCommand('POST', self.factory.path) | |
| 271 self.sendHeader('User-Agent', 'Twisted/XMLRPClib') | |
| 272 self.sendHeader('Host', self.factory.host) | |
| 273 self.sendHeader('Content-type', 'text/xml') | |
| 274 self.sendHeader('Content-length', str(len(self.factory.payload))) | |
| 275 if self.factory.user: | |
| 276 auth = '%s:%s' % (self.factory.user, self.factory.password) | |
| 277 auth = auth.encode('base64').strip() | |
| 278 self.sendHeader('Authorization', 'Basic %s' % (auth,)) | |
| 279 self.endHeaders() | |
| 280 self.transport.write(self.factory.payload) | |
| 281 | |
| 282 def handleStatus(self, version, status, message): | |
| 283 if status != '200': | |
| 284 self.factory.badStatus(status, message) | |
| 285 | |
| 286 def handleResponse(self, contents): | |
| 287 self.factory.parseResponse(contents) | |
| 288 | |
| 289 | |
| 290 payloadTemplate = """<?xml version="1.0"?> | |
| 291 <methodCall> | |
| 292 <methodName>%s</methodName> | |
| 293 %s | |
| 294 </methodCall> | |
| 295 """ | |
| 296 | |
| 297 | |
| 298 class _QueryFactory(protocol.ClientFactory): | |
| 299 | |
| 300 deferred = None | |
| 301 protocol = QueryProtocol | |
| 302 | |
| 303 def __init__(self, path, host, method, user=None, password=None, | |
| 304 allowNone=False, args=()): | |
| 305 self.path, self.host = path, host | |
| 306 self.user, self.password = user, password | |
| 307 self.payload = payloadTemplate % (method, | |
| 308 xmlrpclib.dumps(args, allow_none=allowNone)) | |
| 309 self.deferred = defer.Deferred() | |
| 310 | |
| 311 def parseResponse(self, contents): | |
| 312 if not self.deferred: | |
| 313 return | |
| 314 try: | |
| 315 response = xmlrpclib.loads(contents) | |
| 316 except: | |
| 317 deferred, self.deferred = self.deferred, None | |
| 318 deferred.errback(failure.Failure()) | |
| 319 else: | |
| 320 deferred, self.deferred = self.deferred, None | |
| 321 deferred.callback(response[0][0]) | |
| 322 | |
| 323 def clientConnectionLost(self, _, reason): | |
| 324 if self.deferred is not None: | |
| 325 deferred, self.deferred = self.deferred, None | |
| 326 deferred.errback(reason) | |
| 327 | |
| 328 clientConnectionFailed = clientConnectionLost | |
| 329 | |
| 330 def badStatus(self, status, message): | |
| 331 deferred, self.deferred = self.deferred, None | |
| 332 deferred.errback(ValueError(status, message)) | |
| 333 | |
| 334 | |
| 335 | |
| 336 class Proxy: | |
| 337 """ | |
| 338 A Proxy for making remote XML-RPC calls. | |
| 339 | |
| 340 Pass the URL of the remote XML-RPC server to the constructor. | |
| 341 | |
| 342 Use proxy.callRemote('foobar', *args) to call remote method | |
| 343 'foobar' with *args. | |
| 344 | |
| 345 @ivar queryFactory: object returning a factory for XML-RPC protocol. Mainly | |
| 346 useful for tests. | |
| 347 """ | |
| 348 queryFactory = _QueryFactory | |
| 349 | |
| 350 def __init__(self, url, user=None, password=None, allowNone=False): | |
| 351 """ | |
| 352 @type url: C{str} | |
| 353 @param url: The URL to which to post method calls. Calls will be made | |
| 354 over SSL if the scheme is HTTPS. If netloc contains username or | |
| 355 password information, these will be used to authenticate, as long as | |
| 356 the C{user} and C{password} arguments are not specified. | |
| 357 | |
| 358 @type user: C{str} or None | |
| 359 @param user: The username with which to authenticate with the server | |
| 360 when making calls. If specified, overrides any username information | |
| 361 embedded in C{url}. If not specified, a value may be taken from C{url} | |
| 362 if present. | |
| 363 | |
| 364 @type password: C{str} or None | |
| 365 @param password: The password with which to authenticate with the | |
| 366 server when making calls. If specified, overrides any password | |
| 367 information embedded in C{url}. If not specified, a value may be taken | |
| 368 from C{url} if present. | |
| 369 | |
| 370 @type allowNone: C{bool} or None | |
| 371 @param allowNone: allow the use of None values in parameters. It's | |
| 372 passed to the underlying xmlrpclib implementation. Default to False. | |
| 373 """ | |
| 374 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) | |
| 375 netlocParts = netloc.split('@') | |
| 376 if len(netlocParts) == 2: | |
| 377 userpass = netlocParts.pop(0).split(':') | |
| 378 self.user = userpass.pop(0) | |
| 379 try: | |
| 380 self.password = userpass.pop(0) | |
| 381 except: | |
| 382 self.password = None | |
| 383 else: | |
| 384 self.user = self.password = None | |
| 385 hostport = netlocParts[0].split(':') | |
| 386 self.host = hostport.pop(0) | |
| 387 try: | |
| 388 self.port = int(hostport.pop(0)) | |
| 389 except: | |
| 390 self.port = None | |
| 391 self.path = path | |
| 392 if self.path in ['', None]: | |
| 393 self.path = '/' | |
| 394 self.secure = (scheme == 'https') | |
| 395 if user is not None: | |
| 396 self.user = user | |
| 397 if password is not None: | |
| 398 self.password = password | |
| 399 self.allowNone = allowNone | |
| 400 | |
| 401 def callRemote(self, method, *args): | |
| 402 factory = self.queryFactory( | |
| 403 self.path, self.host, method, self.user, | |
| 404 self.password, self.allowNone, args) | |
| 405 if self.secure: | |
| 406 from twisted.internet import ssl | |
| 407 reactor.connectSSL(self.host, self.port or 443, | |
| 408 factory, ssl.ClientContextFactory()) | |
| 409 else: | |
| 410 reactor.connectTCP(self.host, self.port or 80, factory) | |
| 411 return factory.deferred | |
| 412 | |
| 413 __all__ = ["XMLRPC", "Handler", "NoSuchFunction", "Fault", "Proxy"] | |
| 414 | |
| OLD | NEW |