1
# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
"""A generic resource for publishing objects via XML-RPC.
9
Requires xmlrpclib (comes standard with Python 2.2 and later, otherwise can be
10
downloaded from http://www.pythonware.com/products/xmlrpc/).
12
API Stability: semi-stable
14
Maintainer: U{Itamar Shtull-Trauring<mailto:twisted@itamarst.org>}
16
from __future__ import nested_scopes
18
__version__ = "$Revision: 1.32 $"[11:-2]
25
from twisted.web import resource, server
26
from twisted.internet import defer, protocol, reactor
27
from twisted.python import log, reflect, failure
28
from twisted.web import http
30
# These are deprecated, use the class level definitions
35
# Useful so people don't need to import xmlrpclib directly
36
Fault = xmlrpclib.Fault
37
Binary = xmlrpclib.Binary
38
Boolean = xmlrpclib.Boolean
39
DateTime = xmlrpclib.DateTime
41
class NoSuchFunction(Fault):
42
"""There is no function by the given name."""
47
"""Handle a XML-RPC request and store the state for a request in progress.
49
Override the run() method and return result using self.result,
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
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.
62
def __init__(self, resource, *args):
63
self.resource = resource # the XML-RPC resource we are connected to
64
self.result = defer.Deferred()
68
# event driven equivalent of 'raise UnimplementedError'
69
self.result.errback(NotImplementedError("Implement run() in subclasses"))
72
class XMLRPC(resource.Resource):
73
"""A resource that implements XML-RPC.
75
You probably want to connect this to '/RPC2'.
77
Methods published can return XML-RPC serializable results, Faults,
78
Binary, Boolean, DateTime, Deferreds, or Handler instances.
80
By default methods beginning with 'xmlrpc_' are published.
82
Sub-handlers for prefixed methods (e.g., system.listMethods)
83
can be added with putSubHandler. By default, prefixes are
84
separated with a '.'. Override self.separator to change this.
87
# Error codes for Twisted, if they conflict with yours then
88
# modify them at runtime.
95
def __init__(self, allowNone=False):
96
resource.Resource.__init__(self)
98
self.allowNone = allowNone
100
def putSubHandler(self, prefix, handler):
101
self.subHandlers[prefix] = handler
103
def getSubHandler(self, prefix):
104
return self.subHandlers.get(prefix, None)
106
def getSubHandlerPrefixes(self):
107
return self.subHandlers.keys()
109
def render(self, request):
110
request.content.seek(0, 0)
111
args, functionPath = xmlrpclib.loads(request.content.read())
113
function = self._getFunction(functionPath)
115
self._cbRender(f, request)
117
request.setHeader("content-type", "text/xml")
118
defer.maybeDeferred(function, *args).addErrback(
121
self._cbRender, request
123
return server.NOT_DONE_YET
125
def _cbRender(self, result, request):
126
if isinstance(result, Handler):
127
result = result.result
128
if not isinstance(result, Fault):
131
s = xmlrpclib.dumps(result, methodresponse=True, allow_none=self.allowNone)
133
f = Fault(self.FAILURE, "can't serialize output")
134
s = xmlrpclib.dumps(f, methodresponse=True, allow_none=self.allowNone)
135
request.setHeader("content-length", str(len(s)))
139
def _ebRender(self, failure):
140
if isinstance(failure.value, Fault):
143
return Fault(self.FAILURE, "error")
145
def _getFunction(self, functionPath):
146
"""Given a string, return a function, or raise NoSuchFunction.
148
This returned function will be called, and should return the result
149
of the call, a Deferred, or a Fault instance.
151
Override in subclasses if you want your own policy. The default
152
policy is that given functionPath 'foo', return the method at
153
self.xmlrpc_foo, i.e. getattr(self, "xmlrpc_" + functionPath).
154
If functionPath contains self.separator, the sub-handler for
155
the initial prefix is used to search for the remaining path.
157
if functionPath.find(self.separator) != -1:
158
prefix, functionPath = functionPath.split(self.separator, 1)
159
handler = self.getSubHandler(prefix)
160
if handler is None: raise NoSuchFunction(self.NOT_FOUND, "no such subHandler %s" % prefix)
161
return handler._getFunction(functionPath)
163
f = getattr(self, "xmlrpc_%s" % functionPath, None)
165
raise NoSuchFunction(self.NOT_FOUND, "function %s not found" % functionPath)
166
elif not callable(f):
167
raise NoSuchFunction(self.NOT_FOUND, "function %s not callable" % functionPath)
171
def _listFunctions(self):
172
"""Return a list of the names of all xmlrpc methods."""
173
return reflect.prefixedMethodNames(self.__class__, 'xmlrpc_')
176
class XMLRPCIntrospection(XMLRPC):
177
"""Implement the XML-RPC Introspection API.
179
By default, the methodHelp method returns the 'help' method attribute,
180
if it exists, otherwise the __doc__ method attribute, if it exists,
181
otherwise the empty string.
183
To enable the methodSignature method, add a 'signature' method attribute
184
containing a list of lists. See methodSignature's documentation for the
185
format. Note the type strings should be XML-RPC types, not Python types.
188
def __init__(self, parent):
189
"""Implement Introspection support for an XMLRPC server.
191
@param parent: the XMLRPC server to add Introspection support to.
194
XMLRPC.__init__(self)
195
self._xmlrpc_parent = parent
197
def xmlrpc_listMethods(self):
198
"""Return a list of the method names implemented by this server."""
200
todo = [(self._xmlrpc_parent, '')]
202
obj, prefix = todo.pop(0)
203
functions.extend([ prefix + name for name in obj._listFunctions() ])
204
todo.extend([ (obj.getSubHandler(name),
205
prefix + name + obj.separator)
206
for name in obj.getSubHandlerPrefixes() ])
209
xmlrpc_listMethods.signature = [['array']]
211
def xmlrpc_methodHelp(self, method):
212
"""Return a documentation string describing the use of the given method.
214
method = self._xmlrpc_parent._getFunction(method)
215
return (getattr(method, 'help', None)
216
or getattr(method, '__doc__', None) or '')
218
xmlrpc_methodHelp.signature = [['string', 'string']]
220
def xmlrpc_methodSignature(self, method):
221
"""Return a list of type signatures.
223
Each type signature is a list of the form [rtype, type1, type2, ...]
224
where rtype is the return type and typeN is the type of the Nth
225
argument. If no signature information is available, the empty
228
method = self._xmlrpc_parent._getFunction(method)
229
return getattr(method, 'signature', None) or ''
231
xmlrpc_methodSignature.signature = [['array', 'string'],
232
['string', 'string']]
235
def addIntrospection(xmlrpc):
236
"""Add Introspection support to an XMLRPC server.
238
@param xmlrpc: The xmlrpc server to add Introspection support to.
240
xmlrpc.putSubHandler('system', XMLRPCIntrospection(xmlrpc))
243
class QueryProtocol(http.HTTPClient):
245
def connectionMade(self):
246
self.sendCommand('POST', self.factory.path)
247
self.sendHeader('User-Agent', 'Twisted/XMLRPClib')
248
self.sendHeader('Host', self.factory.host)
249
self.sendHeader('Content-type', 'text/xml')
250
self.sendHeader('Content-length', str(len(self.factory.payload)))
251
if self.factory.user:
252
auth = '%s:%s' % (self.factory.user, self.factory.password)
253
auth = auth.encode('base64').strip()
254
self.sendHeader('Authorization', 'Basic %s' % (auth,))
256
self.transport.write(self.factory.payload)
258
def handleStatus(self, version, status, message):
260
self.factory.badStatus(status, message)
262
def handleResponse(self, contents):
263
self.factory.parseResponse(contents)
266
payloadTemplate = """<?xml version="1.0"?>
268
<methodName>%s</methodName>
274
class _QueryFactory(protocol.ClientFactory):
277
protocol = QueryProtocol
279
def __init__(self, path, host, method, user=None, password=None, allowNone=False, args=()):
280
self.path, self.host = path, host
281
self.user, self.password = user, password
282
self.payload = payloadTemplate % (method, xmlrpclib.dumps(args, allow_none=allowNone))
283
self.deferred = defer.Deferred()
285
def parseResponse(self, contents):
286
if not self.deferred:
289
response = xmlrpclib.loads(contents)
291
self.deferred.errback(failure.Failure())
294
self.deferred.callback(response[0][0])
297
def clientConnectionLost(self, _, reason):
298
if self.deferred is not None:
299
self.deferred.errback(reason)
302
clientConnectionFailed = clientConnectionLost
304
def badStatus(self, status, message):
305
self.deferred.errback(ValueError(status, message))
310
"""A Proxy for making remote XML-RPC calls.
312
Pass the URL of the remote XML-RPC server to the constructor.
314
Use proxy.callRemote('foobar', *args) to call remote method
319
def __init__(self, url, user=None, password=None, allowNone=False):
322
@param url: The URL to which to post method calls. Calls will be made
323
over SSL if the scheme is HTTPS. If netloc contains username or
324
password information, these will be used to authenticate, as long as
325
the C{user} and C{password} arguments are not specified.
327
@type user: C{str} or None
328
@param user: The username with which to authenticate with the server
329
when making calls. If specified, overrides any username information
330
embedded in C{url}. If not specified, a value may be taken from C{url}
333
@type password: C{str} or None
334
@param password: The password with which to authenticate with the
335
server when making calls. If specified, overrides any password
336
information embedded in C{url}. If not specified, a value may be taken
337
from C{url} if present.
339
@type allowNone: C{bool} or None
340
@param allowNone: allow the use of None values in parameters. It's
341
passed to the underlying xmlrpclib implementation. Default to False.
343
scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
344
netlocParts = netloc.split('@')
345
if len(netlocParts) == 2:
346
userpass = netlocParts.pop(0).split(':')
347
self.user = userpass.pop(0)
349
self.password = userpass.pop(0)
353
self.user = self.password = None
354
hostport = netlocParts[0].split(':')
355
self.host = hostport.pop(0)
357
self.port = int(hostport.pop(0))
361
if self.path in ['', None]:
363
self.secure = (scheme == 'https')
366
if password is not None:
367
self.password = password
368
self.allowNone = allowNone
370
def callRemote(self, method, *args):
371
factory = _QueryFactory(
372
self.path, self.host, method, self.user,
373
self.password, self.allowNone, args)
375
from twisted.internet import ssl
376
reactor.connectSSL(self.host, self.port or 443,
377
factory, ssl.ClientContextFactory())
379
reactor.connectTCP(self.host, self.port or 80, factory)
380
return factory.deferred
382
__all__ = ["XMLRPC", "Handler", "NoSuchFunction", "Fault", "Proxy"]