123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633 |
- # -*- test-case-name: twisted.web.test.test_xmlrpc -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- A generic resource for publishing objects via XML-RPC.
- Maintainer: Itamar Shtull-Trauring
- @var Fault: See L{xmlrpclib.Fault}
- @type Fault: L{xmlrpclib.Fault}
- """
- # System Imports
- import base64
- import xmlrpc.client as xmlrpclib
- from urllib.parse import urlparse
- from xmlrpc.client import Binary, Boolean, DateTime, Fault
- from twisted.internet import defer, error, protocol
- from twisted.logger import Logger
- from twisted.python import failure, reflect
- from twisted.python.compat import nativeString
- # Sibling Imports
- from twisted.web import http, resource, server
- # These are deprecated, use the class level definitions
- NOT_FOUND = 8001
- FAILURE = 8002
- def withRequest(f):
- """
- Decorator to cause the request to be passed as the first argument
- to the method.
- If an I{xmlrpc_} method is wrapped with C{withRequest}, the
- request object is passed as the first argument to that method.
- For example::
- @withRequest
- def xmlrpc_echo(self, request, s):
- return s
- @since: 10.2
- """
- f.withRequest = True
- return f
- class NoSuchFunction(Fault):
- """
- There is no function by the given name.
- """
- class Handler:
- """
- Handle a XML-RPC request and store the state for a request in progress.
- Override the run() method and return result using self.result,
- a Deferred.
- We require this class since we're not using threads, so we can't
- encapsulate state in a running function if we're going to have
- to wait for results.
- For example, lets say we want to authenticate against twisted.cred,
- run a LDAP query and then pass its result to a database query, all
- as a result of a single XML-RPC command. We'd use a Handler instance
- to store the state of the running command.
- """
- def __init__(self, resource, *args):
- self.resource = resource # the XML-RPC resource we are connected to
- self.result = defer.Deferred()
- self.run(*args)
- def run(self, *args):
- # event driven equivalent of 'raise UnimplementedError'
- self.result.errback(NotImplementedError("Implement run() in subclasses"))
- class XMLRPC(resource.Resource):
- """
- A resource that implements XML-RPC.
- You probably want to connect this to '/RPC2'.
- Methods published can return XML-RPC serializable results, Faults,
- Binary, Boolean, DateTime, Deferreds, or Handler instances.
- By default methods beginning with 'xmlrpc_' are published.
- Sub-handlers for prefixed methods (e.g., system.listMethods)
- can be added with putSubHandler. By default, prefixes are
- separated with a '.'. Override self.separator to change this.
- @ivar allowNone: Permit XML translating of Python constant None.
- @type allowNone: C{bool}
- @ivar useDateTime: Present C{datetime} values as C{datetime.datetime}
- objects?
- @type useDateTime: C{bool}
- """
- # Error codes for Twisted, if they conflict with yours then
- # modify them at runtime.
- NOT_FOUND = 8001
- FAILURE = 8002
- isLeaf = 1
- separator = "."
- allowedMethods = (b"POST",)
- _log = Logger()
- def __init__(self, allowNone=False, useDateTime=False):
- resource.Resource.__init__(self)
- self.subHandlers = {}
- self.allowNone = allowNone
- self.useDateTime = useDateTime
- def __setattr__(self, name, value):
- self.__dict__[name] = value
- def putSubHandler(self, prefix, handler):
- self.subHandlers[prefix] = handler
- def getSubHandler(self, prefix):
- return self.subHandlers.get(prefix, None)
- def getSubHandlerPrefixes(self):
- return list(self.subHandlers.keys())
- def render_POST(self, request):
- request.content.seek(0, 0)
- request.setHeader(b"content-type", b"text/xml; charset=utf-8")
- try:
- args, functionPath = xmlrpclib.loads(
- request.content.read(), use_datetime=self.useDateTime
- )
- except Exception as e:
- f = Fault(self.FAILURE, f"Can't deserialize input: {e}")
- self._cbRender(f, request)
- else:
- try:
- function = self.lookupProcedure(functionPath)
- except Fault as f:
- self._cbRender(f, request)
- else:
- # Use this list to track whether the response has failed or not.
- # This will be used later on to decide if the result of the
- # Deferred should be written out and Request.finish called.
- responseFailed = []
- request.notifyFinish().addErrback(responseFailed.append)
- if getattr(function, "withRequest", False):
- d = defer.maybeDeferred(function, request, *args)
- else:
- d = defer.maybeDeferred(function, *args)
- d.addErrback(self._ebRender)
- d.addCallback(self._cbRender, request, responseFailed)
- return server.NOT_DONE_YET
- def _cbRender(self, result, request, responseFailed=None):
- if responseFailed:
- return
- if isinstance(result, Handler):
- result = result.result
- if not isinstance(result, Fault):
- result = (result,)
- try:
- try:
- content = xmlrpclib.dumps(
- result, methodresponse=True, allow_none=self.allowNone
- )
- except Exception as e:
- f = Fault(self.FAILURE, f"Can't serialize output: {e}")
- content = xmlrpclib.dumps(
- f, methodresponse=True, allow_none=self.allowNone
- )
- if isinstance(content, str):
- content = content.encode("utf8")
- request.setHeader(b"content-length", b"%d" % (len(content),))
- request.write(content)
- except Exception:
- self._log.failure("")
- request.finish()
- def _ebRender(self, failure):
- if isinstance(failure.value, Fault):
- return failure.value
- self._log.failure("", failure)
- return Fault(self.FAILURE, "error")
- def lookupProcedure(self, procedurePath):
- """
- Given a string naming a procedure, return a callable object for that
- procedure or raise NoSuchFunction.
- The returned object will be called, and should return the result of the
- procedure, a Deferred, or a Fault instance.
- Override in subclasses if you want your own policy. The base
- implementation that given C{'foo'}, C{self.xmlrpc_foo} will be returned.
- If C{procedurePath} contains C{self.separator}, the sub-handler for the
- initial prefix is used to search for the remaining path.
- If you override C{lookupProcedure}, you may also want to override
- C{listProcedures} to accurately report the procedures supported by your
- resource, so that clients using the I{system.listMethods} procedure
- receive accurate results.
- @since: 11.1
- """
- if procedurePath.find(self.separator) != -1:
- prefix, procedurePath = procedurePath.split(self.separator, 1)
- handler = self.getSubHandler(prefix)
- if handler is None:
- raise NoSuchFunction(self.NOT_FOUND, "no such subHandler %s" % prefix)
- return handler.lookupProcedure(procedurePath)
- f = getattr(self, "xmlrpc_%s" % procedurePath, None)
- if not f:
- raise NoSuchFunction(
- self.NOT_FOUND, "procedure %s not found" % procedurePath
- )
- elif not callable(f):
- raise NoSuchFunction(
- self.NOT_FOUND, "procedure %s not callable" % procedurePath
- )
- else:
- return f
- def listProcedures(self):
- """
- Return a list of the names of all xmlrpc procedures.
- @since: 11.1
- """
- return reflect.prefixedMethodNames(self.__class__, "xmlrpc_")
- class XMLRPCIntrospection(XMLRPC):
- """
- Implement the XML-RPC Introspection API.
- By default, the methodHelp method returns the 'help' method attribute,
- if it exists, otherwise the __doc__ method attribute, if it exists,
- otherwise the empty string.
- To enable the methodSignature method, add a 'signature' method attribute
- containing a list of lists. See methodSignature's documentation for the
- format. Note the type strings should be XML-RPC types, not Python types.
- """
- def __init__(self, parent):
- """
- Implement Introspection support for an XMLRPC server.
- @param parent: the XMLRPC server to add Introspection support to.
- @type parent: L{XMLRPC}
- """
- XMLRPC.__init__(self)
- self._xmlrpc_parent = parent
- def xmlrpc_listMethods(self):
- """
- Return a list of the method names implemented by this server.
- """
- functions = []
- todo = [(self._xmlrpc_parent, "")]
- while todo:
- obj, prefix = todo.pop(0)
- functions.extend([prefix + name for name in obj.listProcedures()])
- todo.extend(
- [
- (obj.getSubHandler(name), prefix + name + obj.separator)
- for name in obj.getSubHandlerPrefixes()
- ]
- )
- return functions
- xmlrpc_listMethods.signature = [["array"]] # type: ignore[attr-defined]
- def xmlrpc_methodHelp(self, method):
- """
- Return a documentation string describing the use of the given method.
- """
- method = self._xmlrpc_parent.lookupProcedure(method)
- return getattr(method, "help", None) or getattr(method, "__doc__", None) or ""
- xmlrpc_methodHelp.signature = [["string", "string"]] # type: ignore[attr-defined]
- def xmlrpc_methodSignature(self, method):
- """
- Return a list of type signatures.
- Each type signature is a list of the form [rtype, type1, type2, ...]
- where rtype is the return type and typeN is the type of the Nth
- argument. If no signature information is available, the empty
- string is returned.
- """
- method = self._xmlrpc_parent.lookupProcedure(method)
- return getattr(method, "signature", None) or ""
- xmlrpc_methodSignature.signature = [ # type: ignore[attr-defined]
- ["array", "string"],
- ["string", "string"],
- ]
- def addIntrospection(xmlrpc):
- """
- Add Introspection support to an XMLRPC server.
- @param xmlrpc: the XMLRPC server to add Introspection support to.
- @type xmlrpc: L{XMLRPC}
- """
- xmlrpc.putSubHandler("system", XMLRPCIntrospection(xmlrpc))
- class QueryProtocol(http.HTTPClient):
- def connectionMade(self):
- self._response = None
- self.sendCommand(b"POST", self.factory.path)
- self.sendHeader(b"User-Agent", b"Twisted/XMLRPClib")
- self.sendHeader(b"Host", self.factory.host)
- self.sendHeader(b"Content-type", b"text/xml; charset=utf-8")
- payload = self.factory.payload
- self.sendHeader(b"Content-length", b"%d" % (len(payload),))
- if self.factory.user:
- auth = b":".join([self.factory.user, self.factory.password])
- authHeader = b"".join([b"Basic ", base64.b64encode(auth)])
- self.sendHeader(b"Authorization", authHeader)
- self.endHeaders()
- self.transport.write(payload)
- def handleStatus(self, version, status, message):
- if status != b"200":
- self.factory.badStatus(status, message)
- def handleResponse(self, contents):
- """
- Handle the XML-RPC response received from the server.
- Specifically, disconnect from the server and store the XML-RPC
- response so that it can be properly handled when the disconnect is
- finished.
- """
- self.transport.loseConnection()
- self._response = contents
- def connectionLost(self, reason):
- """
- The connection to the server has been lost.
- If we have a full response from the server, then parse it and fired a
- Deferred with the return value or C{Fault} that the server gave us.
- """
- if not reason.check(error.ConnectionDone, error.ConnectionLost):
- # for example, ssl.SSL.Error
- self.factory.clientConnectionLost(None, reason)
- http.HTTPClient.connectionLost(self, reason)
- if self._response is not None:
- response, self._response = self._response, None
- self.factory.parseResponse(response)
- payloadTemplate = """<?xml version="1.0"?>
- <methodCall>
- <methodName>%s</methodName>
- %s
- </methodCall>
- """
- class QueryFactory(protocol.ClientFactory):
- """
- XML-RPC Client Factory
- @ivar path: The path portion of the URL to which to post method calls.
- @type path: L{bytes}
- @ivar host: The value to use for the Host HTTP header.
- @type host: L{bytes}
- @ivar user: The username with which to authenticate with the server
- when making calls.
- @type user: L{bytes} or L{None}
- @ivar password: The password with which to authenticate with the server
- when making calls.
- @type password: L{bytes} or L{None}
- @ivar useDateTime: Accept datetime values as datetime.datetime objects.
- also passed to the underlying xmlrpclib implementation. Defaults to
- C{False}.
- @type useDateTime: C{bool}
- """
- deferred = None
- protocol = QueryProtocol
- def __init__(
- self,
- path,
- host,
- method,
- user=None,
- password=None,
- allowNone=False,
- args=(),
- canceller=None,
- useDateTime=False,
- ):
- """
- @param method: The name of the method to call.
- @type method: C{str}
- @param allowNone: allow the use of None values in parameters. It's
- passed to the underlying xmlrpclib implementation. Defaults to
- C{False}.
- @type allowNone: C{bool} or L{None}
- @param args: the arguments to pass to the method.
- @type args: C{tuple}
- @param canceller: A 1-argument callable passed to the deferred as the
- canceller callback.
- @type canceller: callable or L{None}
- """
- self.path, self.host = path, host
- self.user, self.password = user, password
- self.payload = payloadTemplate % (
- method,
- xmlrpclib.dumps(args, allow_none=allowNone),
- )
- if isinstance(self.payload, str):
- self.payload = self.payload.encode("utf8")
- self.deferred = defer.Deferred(canceller)
- self.useDateTime = useDateTime
- def parseResponse(self, contents):
- if not self.deferred:
- return
- try:
- response = xmlrpclib.loads(contents, use_datetime=self.useDateTime)[0][0]
- except BaseException:
- deferred, self.deferred = self.deferred, None
- deferred.errback(failure.Failure())
- else:
- deferred, self.deferred = self.deferred, None
- deferred.callback(response)
- def clientConnectionLost(self, _, reason):
- if self.deferred is not None:
- deferred, self.deferred = self.deferred, None
- deferred.errback(reason)
- clientConnectionFailed = clientConnectionLost
- def badStatus(self, status, message):
- deferred, self.deferred = self.deferred, None
- deferred.errback(ValueError(status, message))
- class Proxy:
- """
- A Proxy for making remote XML-RPC calls.
- Pass the URL of the remote XML-RPC server to the constructor.
- Use C{proxy.callRemote('foobar', *args)} to call remote method
- 'foobar' with *args.
- @ivar user: The username with which to authenticate with the server
- when making calls. If specified, overrides any username information
- embedded in C{url}. If not specified, a value may be taken from
- C{url} if present.
- @type user: L{bytes} or L{None}
- @ivar password: The password with which to authenticate with the server
- when making calls. If specified, overrides any password information
- embedded in C{url}. If not specified, a value may be taken from
- C{url} if present.
- @type password: L{bytes} or L{None}
- @ivar allowNone: allow the use of None values in parameters. It's
- passed to the underlying L{xmlrpclib} implementation. Defaults to
- C{False}.
- @type allowNone: C{bool} or L{None}
- @ivar useDateTime: Accept datetime values as datetime.datetime objects.
- also passed to the underlying L{xmlrpclib} implementation. Defaults to
- C{False}.
- @type useDateTime: C{bool}
- @ivar connectTimeout: Number of seconds to wait before assuming the
- connection has failed.
- @type connectTimeout: C{float}
- @ivar _reactor: The reactor used to create connections.
- @type _reactor: Object providing L{twisted.internet.interfaces.IReactorTCP}
- @ivar queryFactory: Object returning a factory for XML-RPC protocol. Use
- this for testing, or to manipulate the XML-RPC parsing behavior. For
- example, you may set this to a custom "debugging" factory object that
- reimplements C{parseResponse} in order to log the raw XML-RPC contents
- from the server before continuing on with parsing. Another possibility
- is to implement your own XML-RPC marshaller here to handle non-standard
- XML-RPC traffic.
- @type queryFactory: L{twisted.web.xmlrpc.QueryFactory}
- """
- queryFactory = QueryFactory
- def __init__(
- self,
- url,
- user=None,
- password=None,
- allowNone=False,
- useDateTime=False,
- connectTimeout=30.0,
- reactor=None,
- ):
- """
- @param url: The URL to which to post method calls. Calls will be made
- over SSL if the scheme is HTTPS. If netloc contains username or
- password information, these will be used to authenticate, as long as
- the C{user} and C{password} arguments are not specified.
- @type url: L{bytes}
- """
- if reactor is None:
- from twisted.internet import reactor
- scheme, netloc, path, params, query, fragment = urlparse(url)
- netlocParts = netloc.split(b"@")
- if len(netlocParts) == 2:
- userpass = netlocParts.pop(0).split(b":")
- self.user = userpass.pop(0)
- try:
- self.password = userpass.pop(0)
- except BaseException:
- self.password = None
- else:
- self.user = self.password = None
- hostport = netlocParts[0].split(b":")
- self.host = hostport.pop(0)
- try:
- self.port = int(hostport.pop(0))
- except BaseException:
- self.port = None
- self.path = path
- if self.path in [b"", None]:
- self.path = b"/"
- self.secure = scheme == b"https"
- if user is not None:
- self.user = user
- if password is not None:
- self.password = password
- self.allowNone = allowNone
- self.useDateTime = useDateTime
- self.connectTimeout = connectTimeout
- self._reactor = reactor
- def callRemote(self, method, *args):
- """
- Call remote XML-RPC C{method} with given arguments.
- @return: a L{defer.Deferred} that will fire with the method response,
- or a failure if the method failed. Generally, the failure type will
- be L{Fault}, but you can also have an C{IndexError} on some buggy
- servers giving empty responses.
- If the deferred is cancelled before the request completes, the
- connection is closed and the deferred will fire with a
- L{defer.CancelledError}.
- """
- def cancel(d):
- factory.deferred = None
- connector.disconnect()
- factory = self.queryFactory(
- self.path,
- self.host,
- method,
- self.user,
- self.password,
- self.allowNone,
- args,
- cancel,
- self.useDateTime,
- )
- if self.secure:
- from twisted.internet import ssl
- contextFactory = ssl.optionsForClientTLS(hostname=nativeString(self.host))
- connector = self._reactor.connectSSL(
- nativeString(self.host),
- self.port or 443,
- factory,
- contextFactory,
- timeout=self.connectTimeout,
- )
- else:
- connector = self._reactor.connectTCP(
- nativeString(self.host),
- self.port or 80,
- factory,
- timeout=self.connectTimeout,
- )
- return factory.deferred
- __all__ = [
- "XMLRPC",
- "Handler",
- "NoSuchFunction",
- "Proxy",
- "Fault",
- "Binary",
- "Boolean",
- "DateTime",
- ]
|