1 # -*- test-case-name: twisted.web.test.test_xmlrpc -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 A generic resource for publishing objects via XML-RPC.
8 Maintainer: Itamar Shtull-Trauring
12 import sys, xmlrpclib, urlparse
16 from twisted.web import resource, server, http
17 from twisted.internet import defer, protocol, reactor
18 from twisted.python import log, reflect, failure
20 # These are deprecated, use the class level definitions
25 # Useful so people don't need to import xmlrpclib directly
26 Fault = xmlrpclib.Fault
27 Binary = xmlrpclib.Binary
28 Boolean = xmlrpclib.Boolean
29 DateTime = xmlrpclib.DateTime
31 # On Python 2.4 and earlier, DateTime.decode returns unicode.
32 if sys.version_info[:2] < (2, 5):
33 _decode = DateTime.decode
34 DateTime.decode = lambda self, value: _decode(self, value.encode('ascii'))
39 Decorator to cause the request to be passed as the first argument
42 If an I{xmlrpc_} method is wrapped with C{withRequest}, the
43 request object is passed as the first argument to that method.
47 def xmlrpc_echo(self, request, s):
57 class NoSuchFunction(Fault):
59 There is no function by the given name.
65 Handle a XML-RPC request and store the state for a request in progress.
67 Override the run() method and return result using self.result,
70 We require this class since we're not using threads, so we can't
71 encapsulate state in a running function if we're going to have
74 For example, lets say we want to authenticate against twisted.cred,
75 run a LDAP query and then pass its result to a database query, all
76 as a result of a single XML-RPC command. We'd use a Handler instance
77 to store the state of the running command.
80 def __init__(self, resource, *args):
81 self.resource = resource # the XML-RPC resource we are connected to
82 self.result = defer.Deferred()
86 # event driven equivalent of 'raise UnimplementedError'
88 NotImplementedError("Implement run() in subclasses"))
91 class XMLRPC(resource.Resource):
93 A resource that implements XML-RPC.
95 You probably want to connect this to '/RPC2'.
97 Methods published can return XML-RPC serializable results, Faults,
98 Binary, Boolean, DateTime, Deferreds, or Handler instances.
100 By default methods beginning with 'xmlrpc_' are published.
102 Sub-handlers for prefixed methods (e.g., system.listMethods)
103 can be added with putSubHandler. By default, prefixes are
104 separated with a '.'. Override self.separator to change this.
106 @ivar allowNone: Permit XML translating of Python constant None.
107 @type allowNone: C{bool}
109 @ivar useDateTime: Present datetime values as datetime.datetime objects?
110 Requires Python >= 2.5.
111 @type useDateTime: C{bool}
114 # Error codes for Twisted, if they conflict with yours then
115 # modify them at runtime.
121 allowedMethods = ('POST',)
123 def __init__(self, allowNone=False, useDateTime=False):
124 resource.Resource.__init__(self)
125 self.subHandlers = {}
126 self.allowNone = allowNone
127 self.useDateTime = useDateTime
130 def __setattr__(self, name, value):
131 if name == "useDateTime" and value and sys.version_info[:2] < (2, 5):
132 raise RuntimeError("useDateTime requires Python 2.5 or later.")
133 self.__dict__[name] = value
136 def putSubHandler(self, prefix, handler):
137 self.subHandlers[prefix] = handler
139 def getSubHandler(self, prefix):
140 return self.subHandlers.get(prefix, None)
142 def getSubHandlerPrefixes(self):
143 return self.subHandlers.keys()
145 def render_POST(self, request):
146 request.content.seek(0, 0)
147 request.setHeader("content-type", "text/xml")
150 args, functionPath = xmlrpclib.loads(request.content.read(),
153 # Maintain backwards compatibility with Python < 2.5
154 args, functionPath = xmlrpclib.loads(request.content.read())
156 f = Fault(self.FAILURE, "Can't deserialize input: %s" % (e,))
157 self._cbRender(f, request)
160 function = self.lookupProcedure(functionPath)
162 self._cbRender(f, request)
164 # Use this list to track whether the response has failed or not.
165 # This will be used later on to decide if the result of the
166 # Deferred should be written out and Request.finish called.
168 request.notifyFinish().addErrback(responseFailed.append)
169 if getattr(function, 'withRequest', False):
170 d = defer.maybeDeferred(function, request, *args)
172 d = defer.maybeDeferred(function, *args)
173 d.addErrback(self._ebRender)
174 d.addCallback(self._cbRender, request, responseFailed)
175 return server.NOT_DONE_YET
178 def _cbRender(self, result, request, responseFailed=None):
182 if isinstance(result, Handler):
183 result = result.result
184 if not isinstance(result, Fault):
188 content = xmlrpclib.dumps(
189 result, methodresponse=True,
190 allow_none=self.allowNone)
192 f = Fault(self.FAILURE, "Can't serialize output: %s" % (e,))
193 content = xmlrpclib.dumps(f, methodresponse=True,
194 allow_none=self.allowNone)
196 request.setHeader("content-length", str(len(content)))
197 request.write(content)
203 def _ebRender(self, failure):
204 if isinstance(failure.value, Fault):
207 return Fault(self.FAILURE, "error")
210 def lookupProcedure(self, procedurePath):
212 Given a string naming a procedure, return a callable object for that
213 procedure or raise NoSuchFunction.
215 The returned object will be called, and should return the result of the
216 procedure, a Deferred, or a Fault instance.
218 Override in subclasses if you want your own policy. The base
219 implementation that given C{'foo'}, C{self.xmlrpc_foo} will be returned.
220 If C{procedurePath} contains C{self.separator}, the sub-handler for the
221 initial prefix is used to search for the remaining path.
223 If you override C{lookupProcedure}, you may also want to override
224 C{listProcedures} to accurately report the procedures supported by your
225 resource, so that clients using the I{system.listMethods} procedure
226 receive accurate results.
230 if procedurePath.find(self.separator) != -1:
231 prefix, procedurePath = procedurePath.split(self.separator, 1)
232 handler = self.getSubHandler(prefix)
234 raise NoSuchFunction(self.NOT_FOUND,
235 "no such subHandler %s" % prefix)
236 return handler.lookupProcedure(procedurePath)
238 f = getattr(self, "xmlrpc_%s" % procedurePath, None)
240 raise NoSuchFunction(self.NOT_FOUND,
241 "procedure %s not found" % procedurePath)
242 elif not callable(f):
243 raise NoSuchFunction(self.NOT_FOUND,
244 "procedure %s not callable" % procedurePath)
248 def listProcedures(self):
250 Return a list of the names of all xmlrpc procedures.
254 return reflect.prefixedMethodNames(self.__class__, 'xmlrpc_')
257 class XMLRPCIntrospection(XMLRPC):
259 Implement the XML-RPC Introspection API.
261 By default, the methodHelp method returns the 'help' method attribute,
262 if it exists, otherwise the __doc__ method attribute, if it exists,
263 otherwise the empty string.
265 To enable the methodSignature method, add a 'signature' method attribute
266 containing a list of lists. See methodSignature's documentation for the
267 format. Note the type strings should be XML-RPC types, not Python types.
270 def __init__(self, parent):
272 Implement Introspection support for an XMLRPC server.
274 @param parent: the XMLRPC server to add Introspection support to.
275 @type parent: L{XMLRPC}
277 XMLRPC.__init__(self)
278 self._xmlrpc_parent = parent
280 def xmlrpc_listMethods(self):
282 Return a list of the method names implemented by this server.
285 todo = [(self._xmlrpc_parent, '')]
287 obj, prefix = todo.pop(0)
288 functions.extend([prefix + name for name in obj.listProcedures()])
289 todo.extend([ (obj.getSubHandler(name),
290 prefix + name + obj.separator)
291 for name in obj.getSubHandlerPrefixes() ])
294 xmlrpc_listMethods.signature = [['array']]
296 def xmlrpc_methodHelp(self, method):
298 Return a documentation string describing the use of the given method.
300 method = self._xmlrpc_parent.lookupProcedure(method)
301 return (getattr(method, 'help', None)
302 or getattr(method, '__doc__', None) or '')
304 xmlrpc_methodHelp.signature = [['string', 'string']]
306 def xmlrpc_methodSignature(self, method):
308 Return a list of type signatures.
310 Each type signature is a list of the form [rtype, type1, type2, ...]
311 where rtype is the return type and typeN is the type of the Nth
312 argument. If no signature information is available, the empty
315 method = self._xmlrpc_parent.lookupProcedure(method)
316 return getattr(method, 'signature', None) or ''
318 xmlrpc_methodSignature.signature = [['array', 'string'],
319 ['string', 'string']]
322 def addIntrospection(xmlrpc):
324 Add Introspection support to an XMLRPC server.
326 @param parent: the XMLRPC server to add Introspection support to.
327 @type parent: L{XMLRPC}
329 xmlrpc.putSubHandler('system', XMLRPCIntrospection(xmlrpc))
332 class QueryProtocol(http.HTTPClient):
334 def connectionMade(self):
335 self._response = None
336 self.sendCommand('POST', self.factory.path)
337 self.sendHeader('User-Agent', 'Twisted/XMLRPClib')
338 self.sendHeader('Host', self.factory.host)
339 self.sendHeader('Content-type', 'text/xml')
340 self.sendHeader('Content-length', str(len(self.factory.payload)))
341 if self.factory.user:
342 auth = '%s:%s' % (self.factory.user, self.factory.password)
343 auth = auth.encode('base64').strip()
344 self.sendHeader('Authorization', 'Basic %s' % (auth,))
346 self.transport.write(self.factory.payload)
348 def handleStatus(self, version, status, message):
350 self.factory.badStatus(status, message)
352 def handleResponse(self, contents):
354 Handle the XML-RPC response received from the server.
356 Specifically, disconnect from the server and store the XML-RPC
357 response so that it can be properly handled when the disconnect is
360 self.transport.loseConnection()
361 self._response = contents
363 def connectionLost(self, reason):
365 The connection to the server has been lost.
367 If we have a full response from the server, then parse it and fired a
368 Deferred with the return value or C{Fault} that the server gave us.
370 http.HTTPClient.connectionLost(self, reason)
371 if self._response is not None:
372 response, self._response = self._response, None
373 self.factory.parseResponse(response)
376 payloadTemplate = """<?xml version="1.0"?>
378 <methodName>%s</methodName>
384 class _QueryFactory(protocol.ClientFactory):
386 XML-RPC Client Factory
388 @ivar path: The path portion of the URL to which to post method calls.
391 @ivar host: The value to use for the Host HTTP header.
394 @ivar user: The username with which to authenticate with the server
396 @type user: C{str} or C{NoneType}
398 @ivar password: The password with which to authenticate with the server
400 @type password: C{str} or C{NoneType}
402 @ivar useDateTime: Accept datetime values as datetime.datetime objects.
403 also passed to the underlying xmlrpclib implementation. Default to
404 False. Requires Python >= 2.5.
405 @type useDateTime: C{bool}
409 protocol = QueryProtocol
411 def __init__(self, path, host, method, user=None, password=None,
412 allowNone=False, args=(), canceller=None, useDateTime=False):
414 @param method: The name of the method to call.
417 @param allowNone: allow the use of None values in parameters. It's
418 passed to the underlying xmlrpclib implementation. Default to False.
419 @type allowNone: C{bool} or C{NoneType}
421 @param args: the arguments to pass to the method.
424 @param canceller: A 1-argument callable passed to the deferred as the
426 @type canceller: callable or C{NoneType}
428 self.path, self.host = path, host
429 self.user, self.password = user, password
430 self.payload = payloadTemplate % (method,
431 xmlrpclib.dumps(args, allow_none=allowNone))
432 self.deferred = defer.Deferred(canceller)
433 self.useDateTime = useDateTime
435 def parseResponse(self, contents):
436 if not self.deferred:
440 response = xmlrpclib.loads(contents,
441 use_datetime=True)[0][0]
443 # Maintain backwards compatibility with Python < 2.5
444 response = xmlrpclib.loads(contents)[0][0]
446 deferred, self.deferred = self.deferred, None
447 deferred.errback(failure.Failure())
449 deferred, self.deferred = self.deferred, None
450 deferred.callback(response)
452 def clientConnectionLost(self, _, reason):
453 if self.deferred is not None:
454 deferred, self.deferred = self.deferred, None
455 deferred.errback(reason)
457 clientConnectionFailed = clientConnectionLost
459 def badStatus(self, status, message):
460 deferred, self.deferred = self.deferred, None
461 deferred.errback(ValueError(status, message))
467 A Proxy for making remote XML-RPC calls.
469 Pass the URL of the remote XML-RPC server to the constructor.
471 Use proxy.callRemote('foobar', *args) to call remote method
474 @ivar user: The username with which to authenticate with the server
475 when making calls. If specified, overrides any username information
476 embedded in C{url}. If not specified, a value may be taken from
478 @type user: C{str} or C{NoneType}
480 @ivar password: The password with which to authenticate with the server
481 when making calls. If specified, overrides any password information
482 embedded in C{url}. If not specified, a value may be taken from
484 @type password: C{str} or C{NoneType}
486 @ivar allowNone: allow the use of None values in parameters. It's
487 passed to the underlying xmlrpclib implementation. Default to False.
488 @type allowNone: C{bool} or C{NoneType}
490 @ivar useDateTime: Accept datetime values as datetime.datetime objects.
491 also passed to the underlying xmlrpclib implementation. Default to
492 False. Requires Python >= 2.5.
493 @type useDateTime: C{bool}
495 @ivar connectTimeout: Number of seconds to wait before assuming the
496 connection has failed.
497 @type connectTimeout: C{float}
499 @ivar _reactor: the reactor used to create connections.
500 @type _reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
502 @ivar queryFactory: object returning a factory for XML-RPC protocol. Mainly
505 queryFactory = _QueryFactory
507 def __init__(self, url, user=None, password=None, allowNone=False,
508 useDateTime=False, connectTimeout=30.0, reactor=reactor):
510 @param url: The URL to which to post method calls. Calls will be made
511 over SSL if the scheme is HTTPS. If netloc contains username or
512 password information, these will be used to authenticate, as long as
513 the C{user} and C{password} arguments are not specified.
517 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
518 netlocParts = netloc.split('@')
519 if len(netlocParts) == 2:
520 userpass = netlocParts.pop(0).split(':')
521 self.user = userpass.pop(0)
523 self.password = userpass.pop(0)
527 self.user = self.password = None
528 hostport = netlocParts[0].split(':')
529 self.host = hostport.pop(0)
531 self.port = int(hostport.pop(0))
535 if self.path in ['', None]:
537 self.secure = (scheme == 'https')
540 if password is not None:
541 self.password = password
542 self.allowNone = allowNone
543 self.useDateTime = useDateTime
544 self.connectTimeout = connectTimeout
545 self._reactor = reactor
548 def __setattr__(self, name, value):
549 if name == "useDateTime" and value and sys.version_info[:2] < (2, 5):
550 raise RuntimeError("useDateTime requires Python 2.5 or later.")
551 self.__dict__[name] = value
554 def callRemote(self, method, *args):
556 Call remote XML-RPC C{method} with given arguments.
558 @return: a L{defer.Deferred} that will fire with the method response,
559 or a failure if the method failed. Generally, the failure type will
560 be L{Fault}, but you can also have an C{IndexError} on some buggy
561 servers giving empty responses.
563 If the deferred is cancelled before the request completes, the
564 connection is closed and the deferred will fire with a
565 L{defer.CancelledError}.
568 factory.deferred = None
569 connector.disconnect()
570 factory = self.queryFactory(
571 self.path, self.host, method, self.user,
572 self.password, self.allowNone, args, cancel, self.useDateTime)
575 from twisted.internet import ssl
576 connector = self._reactor.connectSSL(
577 self.host, self.port or 443,
578 factory, ssl.ClientContextFactory(),
579 timeout=self.connectTimeout)
581 connector = self._reactor.connectTCP(
582 self.host, self.port or 80, factory,
583 timeout=self.connectTimeout)
584 return factory.deferred
588 "XMLRPC", "Handler", "NoSuchFunction", "Proxy",
590 "Fault", "Binary", "Boolean", "DateTime"]