xmlrpc.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. # -*- test-case-name: twisted.web.test.test_xmlrpc -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. A generic resource for publishing objects via XML-RPC.
  6. Maintainer: Itamar Shtull-Trauring
  7. @var Fault: See L{xmlrpclib.Fault}
  8. @type Fault: L{xmlrpclib.Fault}
  9. """
  10. # System Imports
  11. import base64
  12. import xmlrpc.client as xmlrpclib
  13. from urllib.parse import urlparse
  14. from xmlrpc.client import Binary, Boolean, DateTime, Fault
  15. from twisted.internet import defer, error, protocol
  16. from twisted.logger import Logger
  17. from twisted.python import failure, reflect
  18. from twisted.python.compat import nativeString
  19. # Sibling Imports
  20. from twisted.web import http, resource, server
  21. # These are deprecated, use the class level definitions
  22. NOT_FOUND = 8001
  23. FAILURE = 8002
  24. def withRequest(f):
  25. """
  26. Decorator to cause the request to be passed as the first argument
  27. to the method.
  28. If an I{xmlrpc_} method is wrapped with C{withRequest}, the
  29. request object is passed as the first argument to that method.
  30. For example::
  31. @withRequest
  32. def xmlrpc_echo(self, request, s):
  33. return s
  34. @since: 10.2
  35. """
  36. f.withRequest = True
  37. return f
  38. class NoSuchFunction(Fault):
  39. """
  40. There is no function by the given name.
  41. """
  42. class Handler:
  43. """
  44. Handle a XML-RPC request and store the state for a request in progress.
  45. Override the run() method and return result using self.result,
  46. a Deferred.
  47. We require this class since we're not using threads, so we can't
  48. encapsulate state in a running function if we're going to have
  49. to wait for results.
  50. For example, lets say we want to authenticate against twisted.cred,
  51. run a LDAP query and then pass its result to a database query, all
  52. as a result of a single XML-RPC command. We'd use a Handler instance
  53. to store the state of the running command.
  54. """
  55. def __init__(self, resource, *args):
  56. self.resource = resource # the XML-RPC resource we are connected to
  57. self.result = defer.Deferred()
  58. self.run(*args)
  59. def run(self, *args):
  60. # event driven equivalent of 'raise UnimplementedError'
  61. self.result.errback(NotImplementedError("Implement run() in subclasses"))
  62. class XMLRPC(resource.Resource):
  63. """
  64. A resource that implements XML-RPC.
  65. You probably want to connect this to '/RPC2'.
  66. Methods published can return XML-RPC serializable results, Faults,
  67. Binary, Boolean, DateTime, Deferreds, or Handler instances.
  68. By default methods beginning with 'xmlrpc_' are published.
  69. Sub-handlers for prefixed methods (e.g., system.listMethods)
  70. can be added with putSubHandler. By default, prefixes are
  71. separated with a '.'. Override self.separator to change this.
  72. @ivar allowNone: Permit XML translating of Python constant None.
  73. @type allowNone: C{bool}
  74. @ivar useDateTime: Present C{datetime} values as C{datetime.datetime}
  75. objects?
  76. @type useDateTime: C{bool}
  77. """
  78. # Error codes for Twisted, if they conflict with yours then
  79. # modify them at runtime.
  80. NOT_FOUND = 8001
  81. FAILURE = 8002
  82. isLeaf = 1
  83. separator = "."
  84. allowedMethods = (b"POST",)
  85. _log = Logger()
  86. def __init__(self, allowNone=False, useDateTime=False):
  87. resource.Resource.__init__(self)
  88. self.subHandlers = {}
  89. self.allowNone = allowNone
  90. self.useDateTime = useDateTime
  91. def __setattr__(self, name, value):
  92. self.__dict__[name] = value
  93. def putSubHandler(self, prefix, handler):
  94. self.subHandlers[prefix] = handler
  95. def getSubHandler(self, prefix):
  96. return self.subHandlers.get(prefix, None)
  97. def getSubHandlerPrefixes(self):
  98. return list(self.subHandlers.keys())
  99. def render_POST(self, request):
  100. request.content.seek(0, 0)
  101. request.setHeader(b"content-type", b"text/xml; charset=utf-8")
  102. try:
  103. args, functionPath = xmlrpclib.loads(
  104. request.content.read(), use_datetime=self.useDateTime
  105. )
  106. except Exception as e:
  107. f = Fault(self.FAILURE, f"Can't deserialize input: {e}")
  108. self._cbRender(f, request)
  109. else:
  110. try:
  111. function = self.lookupProcedure(functionPath)
  112. except Fault as f:
  113. self._cbRender(f, request)
  114. else:
  115. # Use this list to track whether the response has failed or not.
  116. # This will be used later on to decide if the result of the
  117. # Deferred should be written out and Request.finish called.
  118. responseFailed = []
  119. request.notifyFinish().addErrback(responseFailed.append)
  120. if getattr(function, "withRequest", False):
  121. d = defer.maybeDeferred(function, request, *args)
  122. else:
  123. d = defer.maybeDeferred(function, *args)
  124. d.addErrback(self._ebRender)
  125. d.addCallback(self._cbRender, request, responseFailed)
  126. return server.NOT_DONE_YET
  127. def _cbRender(self, result, request, responseFailed=None):
  128. if responseFailed:
  129. return
  130. if isinstance(result, Handler):
  131. result = result.result
  132. if not isinstance(result, Fault):
  133. result = (result,)
  134. try:
  135. try:
  136. content = xmlrpclib.dumps(
  137. result, methodresponse=True, allow_none=self.allowNone
  138. )
  139. except Exception as e:
  140. f = Fault(self.FAILURE, f"Can't serialize output: {e}")
  141. content = xmlrpclib.dumps(
  142. f, methodresponse=True, allow_none=self.allowNone
  143. )
  144. if isinstance(content, str):
  145. content = content.encode("utf8")
  146. request.setHeader(b"content-length", b"%d" % (len(content),))
  147. request.write(content)
  148. except Exception:
  149. self._log.failure("")
  150. request.finish()
  151. def _ebRender(self, failure):
  152. if isinstance(failure.value, Fault):
  153. return failure.value
  154. self._log.failure("", failure)
  155. return Fault(self.FAILURE, "error")
  156. def lookupProcedure(self, procedurePath):
  157. """
  158. Given a string naming a procedure, return a callable object for that
  159. procedure or raise NoSuchFunction.
  160. The returned object will be called, and should return the result of the
  161. procedure, a Deferred, or a Fault instance.
  162. Override in subclasses if you want your own policy. The base
  163. implementation that given C{'foo'}, C{self.xmlrpc_foo} will be returned.
  164. If C{procedurePath} contains C{self.separator}, the sub-handler for the
  165. initial prefix is used to search for the remaining path.
  166. If you override C{lookupProcedure}, you may also want to override
  167. C{listProcedures} to accurately report the procedures supported by your
  168. resource, so that clients using the I{system.listMethods} procedure
  169. receive accurate results.
  170. @since: 11.1
  171. """
  172. if procedurePath.find(self.separator) != -1:
  173. prefix, procedurePath = procedurePath.split(self.separator, 1)
  174. handler = self.getSubHandler(prefix)
  175. if handler is None:
  176. raise NoSuchFunction(self.NOT_FOUND, "no such subHandler %s" % prefix)
  177. return handler.lookupProcedure(procedurePath)
  178. f = getattr(self, "xmlrpc_%s" % procedurePath, None)
  179. if not f:
  180. raise NoSuchFunction(
  181. self.NOT_FOUND, "procedure %s not found" % procedurePath
  182. )
  183. elif not callable(f):
  184. raise NoSuchFunction(
  185. self.NOT_FOUND, "procedure %s not callable" % procedurePath
  186. )
  187. else:
  188. return f
  189. def listProcedures(self):
  190. """
  191. Return a list of the names of all xmlrpc procedures.
  192. @since: 11.1
  193. """
  194. return reflect.prefixedMethodNames(self.__class__, "xmlrpc_")
  195. class XMLRPCIntrospection(XMLRPC):
  196. """
  197. Implement the XML-RPC Introspection API.
  198. By default, the methodHelp method returns the 'help' method attribute,
  199. if it exists, otherwise the __doc__ method attribute, if it exists,
  200. otherwise the empty string.
  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. def __init__(self, parent):
  206. """
  207. Implement Introspection support for an XMLRPC server.
  208. @param parent: the XMLRPC server to add Introspection support to.
  209. @type parent: L{XMLRPC}
  210. """
  211. XMLRPC.__init__(self)
  212. self._xmlrpc_parent = parent
  213. def xmlrpc_listMethods(self):
  214. """
  215. Return a list of the method names implemented by this server.
  216. """
  217. functions = []
  218. todo = [(self._xmlrpc_parent, "")]
  219. while todo:
  220. obj, prefix = todo.pop(0)
  221. functions.extend([prefix + name for name in obj.listProcedures()])
  222. todo.extend(
  223. [
  224. (obj.getSubHandler(name), prefix + name + obj.separator)
  225. for name in obj.getSubHandlerPrefixes()
  226. ]
  227. )
  228. return functions
  229. xmlrpc_listMethods.signature = [["array"]] # type: ignore[attr-defined]
  230. def xmlrpc_methodHelp(self, method):
  231. """
  232. Return a documentation string describing the use of the given method.
  233. """
  234. method = self._xmlrpc_parent.lookupProcedure(method)
  235. return getattr(method, "help", None) or getattr(method, "__doc__", None) or ""
  236. xmlrpc_methodHelp.signature = [["string", "string"]] # type: ignore[attr-defined]
  237. def xmlrpc_methodSignature(self, method):
  238. """
  239. Return a list of type signatures.
  240. Each type signature is a list of the form [rtype, type1, type2, ...]
  241. where rtype is the return type and typeN is the type of the Nth
  242. argument. If no signature information is available, the empty
  243. string is returned.
  244. """
  245. method = self._xmlrpc_parent.lookupProcedure(method)
  246. return getattr(method, "signature", None) or ""
  247. xmlrpc_methodSignature.signature = [ # type: ignore[attr-defined]
  248. ["array", "string"],
  249. ["string", "string"],
  250. ]
  251. def addIntrospection(xmlrpc):
  252. """
  253. Add Introspection support to an XMLRPC server.
  254. @param xmlrpc: the XMLRPC server to add Introspection support to.
  255. @type xmlrpc: L{XMLRPC}
  256. """
  257. xmlrpc.putSubHandler("system", XMLRPCIntrospection(xmlrpc))
  258. class QueryProtocol(http.HTTPClient):
  259. def connectionMade(self):
  260. self._response = None
  261. self.sendCommand(b"POST", self.factory.path)
  262. self.sendHeader(b"User-Agent", b"Twisted/XMLRPClib")
  263. self.sendHeader(b"Host", self.factory.host)
  264. self.sendHeader(b"Content-type", b"text/xml; charset=utf-8")
  265. payload = self.factory.payload
  266. self.sendHeader(b"Content-length", b"%d" % (len(payload),))
  267. if self.factory.user:
  268. auth = b":".join([self.factory.user, self.factory.password])
  269. authHeader = b"".join([b"Basic ", base64.b64encode(auth)])
  270. self.sendHeader(b"Authorization", authHeader)
  271. self.endHeaders()
  272. self.transport.write(payload)
  273. def handleStatus(self, version, status, message):
  274. if status != b"200":
  275. self.factory.badStatus(status, message)
  276. def handleResponse(self, contents):
  277. """
  278. Handle the XML-RPC response received from the server.
  279. Specifically, disconnect from the server and store the XML-RPC
  280. response so that it can be properly handled when the disconnect is
  281. finished.
  282. """
  283. self.transport.loseConnection()
  284. self._response = contents
  285. def connectionLost(self, reason):
  286. """
  287. The connection to the server has been lost.
  288. If we have a full response from the server, then parse it and fired a
  289. Deferred with the return value or C{Fault} that the server gave us.
  290. """
  291. if not reason.check(error.ConnectionDone, error.ConnectionLost):
  292. # for example, ssl.SSL.Error
  293. self.factory.clientConnectionLost(None, reason)
  294. http.HTTPClient.connectionLost(self, reason)
  295. if self._response is not None:
  296. response, self._response = self._response, None
  297. self.factory.parseResponse(response)
  298. payloadTemplate = """<?xml version="1.0"?>
  299. <methodCall>
  300. <methodName>%s</methodName>
  301. %s
  302. </methodCall>
  303. """
  304. class QueryFactory(protocol.ClientFactory):
  305. """
  306. XML-RPC Client Factory
  307. @ivar path: The path portion of the URL to which to post method calls.
  308. @type path: L{bytes}
  309. @ivar host: The value to use for the Host HTTP header.
  310. @type host: L{bytes}
  311. @ivar user: The username with which to authenticate with the server
  312. when making calls.
  313. @type user: L{bytes} or L{None}
  314. @ivar password: The password with which to authenticate with the server
  315. when making calls.
  316. @type password: L{bytes} or L{None}
  317. @ivar useDateTime: Accept datetime values as datetime.datetime objects.
  318. also passed to the underlying xmlrpclib implementation. Defaults to
  319. C{False}.
  320. @type useDateTime: C{bool}
  321. """
  322. deferred = None
  323. protocol = QueryProtocol
  324. def __init__(
  325. self,
  326. path,
  327. host,
  328. method,
  329. user=None,
  330. password=None,
  331. allowNone=False,
  332. args=(),
  333. canceller=None,
  334. useDateTime=False,
  335. ):
  336. """
  337. @param method: The name of the method to call.
  338. @type method: C{str}
  339. @param allowNone: allow the use of None values in parameters. It's
  340. passed to the underlying xmlrpclib implementation. Defaults to
  341. C{False}.
  342. @type allowNone: C{bool} or L{None}
  343. @param args: the arguments to pass to the method.
  344. @type args: C{tuple}
  345. @param canceller: A 1-argument callable passed to the deferred as the
  346. canceller callback.
  347. @type canceller: callable or L{None}
  348. """
  349. self.path, self.host = path, host
  350. self.user, self.password = user, password
  351. self.payload = payloadTemplate % (
  352. method,
  353. xmlrpclib.dumps(args, allow_none=allowNone),
  354. )
  355. if isinstance(self.payload, str):
  356. self.payload = self.payload.encode("utf8")
  357. self.deferred = defer.Deferred(canceller)
  358. self.useDateTime = useDateTime
  359. def parseResponse(self, contents):
  360. if not self.deferred:
  361. return
  362. try:
  363. response = xmlrpclib.loads(contents, use_datetime=self.useDateTime)[0][0]
  364. except BaseException:
  365. deferred, self.deferred = self.deferred, None
  366. deferred.errback(failure.Failure())
  367. else:
  368. deferred, self.deferred = self.deferred, None
  369. deferred.callback(response)
  370. def clientConnectionLost(self, _, reason):
  371. if self.deferred is not None:
  372. deferred, self.deferred = self.deferred, None
  373. deferred.errback(reason)
  374. clientConnectionFailed = clientConnectionLost
  375. def badStatus(self, status, message):
  376. deferred, self.deferred = self.deferred, None
  377. deferred.errback(ValueError(status, message))
  378. class Proxy:
  379. """
  380. A Proxy for making remote XML-RPC calls.
  381. Pass the URL of the remote XML-RPC server to the constructor.
  382. Use C{proxy.callRemote('foobar', *args)} to call remote method
  383. 'foobar' with *args.
  384. @ivar user: The username with which to authenticate with the server
  385. when making calls. If specified, overrides any username information
  386. embedded in C{url}. If not specified, a value may be taken from
  387. C{url} if present.
  388. @type user: L{bytes} or L{None}
  389. @ivar password: The password with which to authenticate with the server
  390. when making calls. If specified, overrides any password information
  391. embedded in C{url}. If not specified, a value may be taken from
  392. C{url} if present.
  393. @type password: L{bytes} or L{None}
  394. @ivar allowNone: allow the use of None values in parameters. It's
  395. passed to the underlying L{xmlrpclib} implementation. Defaults to
  396. C{False}.
  397. @type allowNone: C{bool} or L{None}
  398. @ivar useDateTime: Accept datetime values as datetime.datetime objects.
  399. also passed to the underlying L{xmlrpclib} implementation. Defaults to
  400. C{False}.
  401. @type useDateTime: C{bool}
  402. @ivar connectTimeout: Number of seconds to wait before assuming the
  403. connection has failed.
  404. @type connectTimeout: C{float}
  405. @ivar _reactor: The reactor used to create connections.
  406. @type _reactor: Object providing L{twisted.internet.interfaces.IReactorTCP}
  407. @ivar queryFactory: Object returning a factory for XML-RPC protocol. Use
  408. this for testing, or to manipulate the XML-RPC parsing behavior. For
  409. example, you may set this to a custom "debugging" factory object that
  410. reimplements C{parseResponse} in order to log the raw XML-RPC contents
  411. from the server before continuing on with parsing. Another possibility
  412. is to implement your own XML-RPC marshaller here to handle non-standard
  413. XML-RPC traffic.
  414. @type queryFactory: L{twisted.web.xmlrpc.QueryFactory}
  415. """
  416. queryFactory = QueryFactory
  417. def __init__(
  418. self,
  419. url,
  420. user=None,
  421. password=None,
  422. allowNone=False,
  423. useDateTime=False,
  424. connectTimeout=30.0,
  425. reactor=None,
  426. ):
  427. """
  428. @param url: The URL to which to post method calls. Calls will be made
  429. over SSL if the scheme is HTTPS. If netloc contains username or
  430. password information, these will be used to authenticate, as long as
  431. the C{user} and C{password} arguments are not specified.
  432. @type url: L{bytes}
  433. """
  434. if reactor is None:
  435. from twisted.internet import reactor
  436. scheme, netloc, path, params, query, fragment = urlparse(url)
  437. netlocParts = netloc.split(b"@")
  438. if len(netlocParts) == 2:
  439. userpass = netlocParts.pop(0).split(b":")
  440. self.user = userpass.pop(0)
  441. try:
  442. self.password = userpass.pop(0)
  443. except BaseException:
  444. self.password = None
  445. else:
  446. self.user = self.password = None
  447. hostport = netlocParts[0].split(b":")
  448. self.host = hostport.pop(0)
  449. try:
  450. self.port = int(hostport.pop(0))
  451. except BaseException:
  452. self.port = None
  453. self.path = path
  454. if self.path in [b"", None]:
  455. self.path = b"/"
  456. self.secure = scheme == b"https"
  457. if user is not None:
  458. self.user = user
  459. if password is not None:
  460. self.password = password
  461. self.allowNone = allowNone
  462. self.useDateTime = useDateTime
  463. self.connectTimeout = connectTimeout
  464. self._reactor = reactor
  465. def callRemote(self, method, *args):
  466. """
  467. Call remote XML-RPC C{method} with given arguments.
  468. @return: a L{defer.Deferred} that will fire with the method response,
  469. or a failure if the method failed. Generally, the failure type will
  470. be L{Fault}, but you can also have an C{IndexError} on some buggy
  471. servers giving empty responses.
  472. If the deferred is cancelled before the request completes, the
  473. connection is closed and the deferred will fire with a
  474. L{defer.CancelledError}.
  475. """
  476. def cancel(d):
  477. factory.deferred = None
  478. connector.disconnect()
  479. factory = self.queryFactory(
  480. self.path,
  481. self.host,
  482. method,
  483. self.user,
  484. self.password,
  485. self.allowNone,
  486. args,
  487. cancel,
  488. self.useDateTime,
  489. )
  490. if self.secure:
  491. from twisted.internet import ssl
  492. contextFactory = ssl.optionsForClientTLS(hostname=nativeString(self.host))
  493. connector = self._reactor.connectSSL(
  494. nativeString(self.host),
  495. self.port or 443,
  496. factory,
  497. contextFactory,
  498. timeout=self.connectTimeout,
  499. )
  500. else:
  501. connector = self._reactor.connectTCP(
  502. nativeString(self.host),
  503. self.port or 80,
  504. factory,
  505. timeout=self.connectTimeout,
  506. )
  507. return factory.deferred
  508. __all__ = [
  509. "XMLRPC",
  510. "Handler",
  511. "NoSuchFunction",
  512. "Proxy",
  513. "Fault",
  514. "Binary",
  515. "Boolean",
  516. "DateTime",
  517. ]