root/tags/releases/twisted-8.2.0/twisted/web/xmlrpc.py

Revision 24679, 13.9 KB (checked in by therve, 2 years ago)

Merge xmlrpc-empty-response-3399

Author: therve
Reviewer: exarkun
Fixes #3399

Fix xmlrpc client so that he manages to return an error for buggy servers
sending an empty response.

Line 
1# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
2# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6A generic resource for publishing objects via XML-RPC.
7
8Maintainer: Itamar Shtull-Trauring
9"""
10
11# System Imports
12import xmlrpclib
13import urlparse
14
15# Sibling Imports
16from twisted.web import resource, server, http
17from twisted.internet import defer, protocol, reactor
18from twisted.python import log, reflect, failure
19
20# These are deprecated, use the class level definitions
21NOT_FOUND = 8001
22FAILURE = 8002
23
24
25# Useful so people don't need to import xmlrpclib directly
26Fault = xmlrpclib.Fault
27Binary = xmlrpclib.Binary
28Boolean = xmlrpclib.Boolean
29DateTime = xmlrpclib.DateTime
30
31class NoSuchFunction(Fault):
32    """
33    There is no function by the given name.
34    """
35
36
37class 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 # the XML-RPC resource we are connected to
56        self.result = defer.Deferred()
57        self.run(*args)
58
59    def run(self, *args):
60        # event driven equivalent of 'raise UnimplementedError'
61        self.result.errback(
62            NotImplementedError("Implement run() in subclasses"))
63
64
65class 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    # Error codes for Twisted, if they conflict with yours then
82    # modify them at runtime.
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
185class 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
250def 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
259class 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
282payloadTemplate = """<?xml version="1.0"?>
283<methodCall>
284<methodName>%s</methodName>
285%s
286</methodCall>
287"""
288
289
290class _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
328class 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"]
Note: See TracBrowser for help on using the browser.