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