~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web/xmlrpc.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
 
2
#
 
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
4
# See LICENSE for details.
 
5
 
 
6
 
 
7
"""A generic resource for publishing objects via XML-RPC.
 
8
 
 
9
Requires xmlrpclib (comes standard with Python 2.2 and later, otherwise can be
 
10
downloaded from http://www.pythonware.com/products/xmlrpc/).
 
11
 
 
12
API Stability: semi-stable
 
13
 
 
14
Maintainer: U{Itamar Shtull-Trauring<mailto:twisted@itamarst.org>}
 
15
"""
 
16
from __future__ import nested_scopes
 
17
 
 
18
__version__ = "$Revision: 1.32 $"[11:-2]
 
19
 
 
20
# System Imports
 
21
import xmlrpclib
 
22
import urlparse
 
23
 
 
24
# Sibling Imports
 
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
 
29
 
 
30
# These are deprecated, use the class level definitions
 
31
NOT_FOUND = 8001
 
32
FAILURE = 8002
 
33
 
 
34
 
 
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
 
40
 
 
41
class NoSuchFunction(Fault):
 
42
    """There is no function by the given name."""
 
43
    pass
 
44
 
 
45
 
 
46
class Handler:
 
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(NotImplementedError("Implement run() in subclasses"))
 
70
 
 
71
 
 
72
class XMLRPC(resource.Resource):
 
73
    """A resource that implements XML-RPC.
 
74
    
 
75
    You probably want to connect this to '/RPC2'.
 
76
 
 
77
    Methods published can return XML-RPC serializable results, Faults,
 
78
    Binary, Boolean, DateTime, Deferreds, or Handler instances.
 
79
 
 
80
    By default methods beginning with 'xmlrpc_' are published.
 
81
 
 
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.
 
85
    """
 
86
 
 
87
    # Error codes for Twisted, if they conflict with yours then
 
88
    # modify them at runtime.
 
89
    NOT_FOUND = 8001
 
90
    FAILURE = 8002
 
91
 
 
92
    isLeaf = 1
 
93
    separator = '.'
 
94
 
 
95
    def __init__(self, allowNone=False):
 
96
        resource.Resource.__init__(self)
 
97
        self.subHandlers = {}
 
98
        self.allowNone = allowNone
 
99
 
 
100
    def putSubHandler(self, prefix, handler):
 
101
        self.subHandlers[prefix] = handler
 
102
 
 
103
    def getSubHandler(self, prefix):
 
104
        return self.subHandlers.get(prefix, None)
 
105
 
 
106
    def getSubHandlerPrefixes(self):
 
107
        return self.subHandlers.keys()
 
108
 
 
109
    def render(self, request):
 
110
        request.content.seek(0, 0)
 
111
        args, functionPath = xmlrpclib.loads(request.content.read())
 
112
        try:
 
113
            function = self._getFunction(functionPath)
 
114
        except Fault, f:
 
115
            self._cbRender(f, request)
 
116
        else:
 
117
            request.setHeader("content-type", "text/xml")
 
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, allow_none=self.allowNone)
 
132
        except:
 
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)))
 
136
        request.write(s)
 
137
        request.finish()
 
138
 
 
139
    def _ebRender(self, failure):
 
140
        if isinstance(failure.value, Fault):
 
141
            return failure.value
 
142
        log.err(failure)
 
143
        return Fault(self.FAILURE, "error")
 
144
 
 
145
    def _getFunction(self, functionPath):
 
146
        """Given a string, return a function, or raise NoSuchFunction.
 
147
 
 
148
        This returned function will be called, and should return the result
 
149
        of the call, a Deferred, or a Fault instance.
 
150
 
 
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.
 
156
        """
 
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)
 
162
 
 
163
        f = getattr(self, "xmlrpc_%s" % functionPath, None)
 
164
        if not f:
 
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)
 
168
        else:
 
169
            return f
 
170
 
 
171
    def _listFunctions(self):
 
172
        """Return a list of the names of all xmlrpc methods."""
 
173
        return reflect.prefixedMethodNames(self.__class__, 'xmlrpc_')
 
174
 
 
175
 
 
176
class XMLRPCIntrospection(XMLRPC):
 
177
    """Implement the XML-RPC Introspection API.
 
178
 
 
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.
 
182
 
 
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.
 
186
    """
 
187
 
 
188
    def __init__(self, parent):
 
189
        """Implement Introspection support for an XMLRPC server.
 
190
 
 
191
        @param parent: the XMLRPC server to add Introspection support to.
 
192
        """
 
193
 
 
194
        XMLRPC.__init__(self)
 
195
        self._xmlrpc_parent = parent
 
196
 
 
197
    def xmlrpc_listMethods(self):
 
198
        """Return a list of the method names implemented by this server."""
 
199
        functions = []
 
200
        todo = [(self._xmlrpc_parent, '')]
 
201
        while todo:
 
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() ])
 
207
        return functions
 
208
 
 
209
    xmlrpc_listMethods.signature = [['array']]
 
210
 
 
211
    def xmlrpc_methodHelp(self, method):
 
212
        """Return a documentation string describing the use of the given method.
 
213
        """
 
214
        method = self._xmlrpc_parent._getFunction(method)
 
215
        return (getattr(method, 'help', None)
 
216
                or getattr(method, '__doc__', None) or '')
 
217
 
 
218
    xmlrpc_methodHelp.signature = [['string', 'string']]
 
219
 
 
220
    def xmlrpc_methodSignature(self, method):
 
221
        """Return a list of type signatures.
 
222
 
 
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
 
226
        string is returned.
 
227
        """
 
228
        method = self._xmlrpc_parent._getFunction(method)
 
229
        return getattr(method, 'signature', None) or ''
 
230
 
 
231
    xmlrpc_methodSignature.signature = [['array', 'string'],
 
232
                                        ['string', 'string']]
 
233
 
 
234
 
 
235
def addIntrospection(xmlrpc):
 
236
    """Add Introspection support to an XMLRPC server.
 
237
 
 
238
    @param xmlrpc: The xmlrpc server to add Introspection support to.
 
239
    """
 
240
    xmlrpc.putSubHandler('system', XMLRPCIntrospection(xmlrpc))
 
241
 
 
242
 
 
243
class QueryProtocol(http.HTTPClient):
 
244
 
 
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,))
 
255
        self.endHeaders()
 
256
        self.transport.write(self.factory.payload)
 
257
 
 
258
    def handleStatus(self, version, status, message):
 
259
        if status != '200':
 
260
            self.factory.badStatus(status, message)
 
261
 
 
262
    def handleResponse(self, contents):
 
263
        self.factory.parseResponse(contents)
 
264
 
 
265
 
 
266
payloadTemplate = """<?xml version="1.0"?>
 
267
<methodCall>
 
268
<methodName>%s</methodName>
 
269
%s
 
270
</methodCall>
 
271
"""
 
272
 
 
273
 
 
274
class _QueryFactory(protocol.ClientFactory):
 
275
 
 
276
    deferred = None
 
277
    protocol = QueryProtocol
 
278
 
 
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()
 
284
 
 
285
    def parseResponse(self, contents):
 
286
        if not self.deferred:
 
287
            return
 
288
        try:
 
289
            response = xmlrpclib.loads(contents)
 
290
        except:
 
291
            self.deferred.errback(failure.Failure())
 
292
            self.deferred = None
 
293
        else:
 
294
            self.deferred.callback(response[0][0])
 
295
            self.deferred = None
 
296
 
 
297
    def clientConnectionLost(self, _, reason):
 
298
        if self.deferred is not None:
 
299
            self.deferred.errback(reason)
 
300
            self.deferred = None
 
301
 
 
302
    clientConnectionFailed = clientConnectionLost
 
303
 
 
304
    def badStatus(self, status, message):
 
305
        self.deferred.errback(ValueError(status, message))
 
306
        self.deferred = None
 
307
 
 
308
 
 
309
class Proxy:
 
310
    """A Proxy for making remote XML-RPC calls.
 
311
 
 
312
    Pass the URL of the remote XML-RPC server to the constructor.
 
313
 
 
314
    Use proxy.callRemote('foobar', *args) to call remote method
 
315
    'foobar' with *args.
 
316
 
 
317
    """
 
318
 
 
319
    def __init__(self, url, user=None, password=None, allowNone=False):
 
320
        """
 
321
        @type url: C{str}
 
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.
 
326
 
 
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}
 
331
        if present.
 
332
 
 
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.
 
338
 
 
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.
 
342
        """
 
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)
 
348
            try:
 
349
                self.password = userpass.pop(0)
 
350
            except:
 
351
                self.password = None
 
352
        else:
 
353
            self.user = self.password = None
 
354
        hostport = netlocParts[0].split(':')
 
355
        self.host = hostport.pop(0)
 
356
        try:
 
357
            self.port = int(hostport.pop(0))
 
358
        except:
 
359
            self.port = None
 
360
        self.path = path
 
361
        if self.path in ['', None]:
 
362
            self.path = '/'
 
363
        self.secure = (scheme == 'https')
 
364
        if user is not None:
 
365
            self.user = user
 
366
        if password is not None:
 
367
            self.password = password
 
368
        self.allowNone = allowNone
 
369
 
 
370
    def callRemote(self, method, *args):
 
371
        factory = _QueryFactory(
 
372
            self.path, self.host, method, self.user,
 
373
            self.password, self.allowNone, args)
 
374
        if self.secure:
 
375
            from twisted.internet import ssl
 
376
            reactor.connectSSL(self.host, self.port or 443,
 
377
                               factory, ssl.ClientContextFactory())
 
378
        else:
 
379
            reactor.connectTCP(self.host, self.port or 80, factory)
 
380
        return factory.deferred
 
381
 
 
382
__all__ = ["XMLRPC", "Handler", "NoSuchFunction", "Fault", "Proxy"]