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