jpayne@68: # jpayne@68: # XML-RPC CLIENT LIBRARY jpayne@68: # $Id$ jpayne@68: # jpayne@68: # an XML-RPC client interface for Python. jpayne@68: # jpayne@68: # the marshalling and response parser code can also be used to jpayne@68: # implement XML-RPC servers. jpayne@68: # jpayne@68: # Notes: jpayne@68: # this version is designed to work with Python 2.1 or newer. jpayne@68: # jpayne@68: # History: jpayne@68: # 1999-01-14 fl Created jpayne@68: # 1999-01-15 fl Changed dateTime to use localtime jpayne@68: # 1999-01-16 fl Added Binary/base64 element, default to RPC2 service jpayne@68: # 1999-01-19 fl Fixed array data element (from Skip Montanaro) jpayne@68: # 1999-01-21 fl Fixed dateTime constructor, etc. jpayne@68: # 1999-02-02 fl Added fault handling, handle empty sequences, etc. jpayne@68: # 1999-02-10 fl Fixed problem with empty responses (from Skip Montanaro) jpayne@68: # 1999-06-20 fl Speed improvements, pluggable parsers/transports (0.9.8) jpayne@68: # 2000-11-28 fl Changed boolean to check the truth value of its argument jpayne@68: # 2001-02-24 fl Added encoding/Unicode/SafeTransport patches jpayne@68: # 2001-02-26 fl Added compare support to wrappers (0.9.9/1.0b1) jpayne@68: # 2001-03-28 fl Make sure response tuple is a singleton jpayne@68: # 2001-03-29 fl Don't require empty params element (from Nicholas Riley) jpayne@68: # 2001-06-10 fl Folded in _xmlrpclib accelerator support (1.0b2) jpayne@68: # 2001-08-20 fl Base xmlrpclib.Error on built-in Exception (from Paul Prescod) jpayne@68: # 2001-09-03 fl Allow Transport subclass to override getparser jpayne@68: # 2001-09-10 fl Lazy import of urllib, cgi, xmllib (20x import speedup) jpayne@68: # 2001-10-01 fl Remove containers from memo cache when done with them jpayne@68: # 2001-10-01 fl Use faster escape method (80% dumps speedup) jpayne@68: # 2001-10-02 fl More dumps microtuning jpayne@68: # 2001-10-04 fl Make sure import expat gets a parser (from Guido van Rossum) jpayne@68: # 2001-10-10 sm Allow long ints to be passed as ints if they don't overflow jpayne@68: # 2001-10-17 sm Test for int and long overflow (allows use on 64-bit systems) jpayne@68: # 2001-11-12 fl Use repr() to marshal doubles (from Paul Felix) jpayne@68: # 2002-03-17 fl Avoid buffered read when possible (from James Rucker) jpayne@68: # 2002-04-07 fl Added pythondoc comments jpayne@68: # 2002-04-16 fl Added __str__ methods to datetime/binary wrappers jpayne@68: # 2002-05-15 fl Added error constants (from Andrew Kuchling) jpayne@68: # 2002-06-27 fl Merged with Python CVS version jpayne@68: # 2002-10-22 fl Added basic authentication (based on code from Phillip Eby) jpayne@68: # 2003-01-22 sm Add support for the bool type jpayne@68: # 2003-02-27 gvr Remove apply calls jpayne@68: # 2003-04-24 sm Use cStringIO if available jpayne@68: # 2003-04-25 ak Add support for nil jpayne@68: # 2003-06-15 gn Add support for time.struct_time jpayne@68: # 2003-07-12 gp Correct marshalling of Faults jpayne@68: # 2003-10-31 mvl Add multicall support jpayne@68: # 2004-08-20 mvl Bump minimum supported Python version to 2.1 jpayne@68: # 2014-12-02 ch/doko Add workaround for gzip bomb vulnerability jpayne@68: # jpayne@68: # Copyright (c) 1999-2002 by Secret Labs AB. jpayne@68: # Copyright (c) 1999-2002 by Fredrik Lundh. jpayne@68: # jpayne@68: # info@pythonware.com jpayne@68: # http://www.pythonware.com jpayne@68: # jpayne@68: # -------------------------------------------------------------------- jpayne@68: # The XML-RPC client interface is jpayne@68: # jpayne@68: # Copyright (c) 1999-2002 by Secret Labs AB jpayne@68: # Copyright (c) 1999-2002 by Fredrik Lundh jpayne@68: # jpayne@68: # By obtaining, using, and/or copying this software and/or its jpayne@68: # associated documentation, you agree that you have read, understood, jpayne@68: # and will comply with the following terms and conditions: jpayne@68: # jpayne@68: # Permission to use, copy, modify, and distribute this software and jpayne@68: # its associated documentation for any purpose and without fee is jpayne@68: # hereby granted, provided that the above copyright notice appears in jpayne@68: # all copies, and that both that copyright notice and this permission jpayne@68: # notice appear in supporting documentation, and that the name of jpayne@68: # Secret Labs AB or the author not be used in advertising or publicity jpayne@68: # pertaining to distribution of the software without specific, written jpayne@68: # prior permission. jpayne@68: # jpayne@68: # SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD jpayne@68: # TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- jpayne@68: # ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR jpayne@68: # BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY jpayne@68: # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, jpayne@68: # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS jpayne@68: # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE jpayne@68: # OF THIS SOFTWARE. jpayne@68: # -------------------------------------------------------------------- jpayne@68: jpayne@68: """ jpayne@68: An XML-RPC client interface for Python. jpayne@68: jpayne@68: The marshalling and response parser code can also be used to jpayne@68: implement XML-RPC servers. jpayne@68: jpayne@68: Exported exceptions: jpayne@68: jpayne@68: Error Base class for client errors jpayne@68: ProtocolError Indicates an HTTP protocol error jpayne@68: ResponseError Indicates a broken response package jpayne@68: Fault Indicates an XML-RPC fault package jpayne@68: jpayne@68: Exported classes: jpayne@68: jpayne@68: ServerProxy Represents a logical connection to an XML-RPC server jpayne@68: jpayne@68: MultiCall Executor of boxcared xmlrpc requests jpayne@68: DateTime dateTime wrapper for an ISO 8601 string or time tuple or jpayne@68: localtime integer value to generate a "dateTime.iso8601" jpayne@68: XML-RPC value jpayne@68: Binary binary data wrapper jpayne@68: jpayne@68: Marshaller Generate an XML-RPC params chunk from a Python data structure jpayne@68: Unmarshaller Unmarshal an XML-RPC response from incoming XML event message jpayne@68: Transport Handles an HTTP transaction to an XML-RPC server jpayne@68: SafeTransport Handles an HTTPS transaction to an XML-RPC server jpayne@68: jpayne@68: Exported constants: jpayne@68: jpayne@68: (none) jpayne@68: jpayne@68: Exported functions: jpayne@68: jpayne@68: getparser Create instance of the fastest available parser & attach jpayne@68: to an unmarshalling object jpayne@68: dumps Convert an argument tuple or a Fault instance to an XML-RPC jpayne@68: request (or response, if the methodresponse option is used). jpayne@68: loads Convert an XML-RPC packet to unmarshalled data plus a method jpayne@68: name (None if not present). jpayne@68: """ jpayne@68: jpayne@68: import base64 jpayne@68: import sys jpayne@68: import time jpayne@68: from datetime import datetime jpayne@68: from decimal import Decimal jpayne@68: import http.client jpayne@68: import urllib.parse jpayne@68: from xml.parsers import expat jpayne@68: import errno jpayne@68: from io import BytesIO jpayne@68: try: jpayne@68: import gzip jpayne@68: except ImportError: jpayne@68: gzip = None #python can be built without zlib/gzip support jpayne@68: jpayne@68: # -------------------------------------------------------------------- jpayne@68: # Internal stuff jpayne@68: jpayne@68: def escape(s): jpayne@68: s = s.replace("&", "&") jpayne@68: s = s.replace("<", "<") jpayne@68: return s.replace(">", ">",) jpayne@68: jpayne@68: # used in User-Agent header sent jpayne@68: __version__ = '%d.%d' % sys.version_info[:2] jpayne@68: jpayne@68: # xmlrpc integer limits jpayne@68: MAXINT = 2**31-1 jpayne@68: MININT = -2**31 jpayne@68: jpayne@68: # -------------------------------------------------------------------- jpayne@68: # Error constants (from Dan Libby's specification at jpayne@68: # http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php) jpayne@68: jpayne@68: # Ranges of errors jpayne@68: PARSE_ERROR = -32700 jpayne@68: SERVER_ERROR = -32600 jpayne@68: APPLICATION_ERROR = -32500 jpayne@68: SYSTEM_ERROR = -32400 jpayne@68: TRANSPORT_ERROR = -32300 jpayne@68: jpayne@68: # Specific errors jpayne@68: NOT_WELLFORMED_ERROR = -32700 jpayne@68: UNSUPPORTED_ENCODING = -32701 jpayne@68: INVALID_ENCODING_CHAR = -32702 jpayne@68: INVALID_XMLRPC = -32600 jpayne@68: METHOD_NOT_FOUND = -32601 jpayne@68: INVALID_METHOD_PARAMS = -32602 jpayne@68: INTERNAL_ERROR = -32603 jpayne@68: jpayne@68: # -------------------------------------------------------------------- jpayne@68: # Exceptions jpayne@68: jpayne@68: ## jpayne@68: # Base class for all kinds of client-side errors. jpayne@68: jpayne@68: class Error(Exception): jpayne@68: """Base class for client errors.""" jpayne@68: __str__ = object.__str__ jpayne@68: jpayne@68: ## jpayne@68: # Indicates an HTTP-level protocol error. This is raised by the HTTP jpayne@68: # transport layer, if the server returns an error code other than 200 jpayne@68: # (OK). jpayne@68: # jpayne@68: # @param url The target URL. jpayne@68: # @param errcode The HTTP error code. jpayne@68: # @param errmsg The HTTP error message. jpayne@68: # @param headers The HTTP header dictionary. jpayne@68: jpayne@68: class ProtocolError(Error): jpayne@68: """Indicates an HTTP protocol error.""" jpayne@68: def __init__(self, url, errcode, errmsg, headers): jpayne@68: Error.__init__(self) jpayne@68: self.url = url jpayne@68: self.errcode = errcode jpayne@68: self.errmsg = errmsg jpayne@68: self.headers = headers jpayne@68: def __repr__(self): jpayne@68: return ( jpayne@68: "<%s for %s: %s %s>" % jpayne@68: (self.__class__.__name__, self.url, self.errcode, self.errmsg) jpayne@68: ) jpayne@68: jpayne@68: ## jpayne@68: # Indicates a broken XML-RPC response package. This exception is jpayne@68: # raised by the unmarshalling layer, if the XML-RPC response is jpayne@68: # malformed. jpayne@68: jpayne@68: class ResponseError(Error): jpayne@68: """Indicates a broken response package.""" jpayne@68: pass jpayne@68: jpayne@68: ## jpayne@68: # Indicates an XML-RPC fault response package. This exception is jpayne@68: # raised by the unmarshalling layer, if the XML-RPC response contains jpayne@68: # a fault string. This exception can also be used as a class, to jpayne@68: # generate a fault XML-RPC message. jpayne@68: # jpayne@68: # @param faultCode The XML-RPC fault code. jpayne@68: # @param faultString The XML-RPC fault string. jpayne@68: jpayne@68: class Fault(Error): jpayne@68: """Indicates an XML-RPC fault package.""" jpayne@68: def __init__(self, faultCode, faultString, **extra): jpayne@68: Error.__init__(self) jpayne@68: self.faultCode = faultCode jpayne@68: self.faultString = faultString jpayne@68: def __repr__(self): jpayne@68: return "<%s %s: %r>" % (self.__class__.__name__, jpayne@68: self.faultCode, self.faultString) jpayne@68: jpayne@68: # -------------------------------------------------------------------- jpayne@68: # Special values jpayne@68: jpayne@68: ## jpayne@68: # Backwards compatibility jpayne@68: jpayne@68: boolean = Boolean = bool jpayne@68: jpayne@68: ## jpayne@68: # Wrapper for XML-RPC DateTime values. This converts a time value to jpayne@68: # the format used by XML-RPC. jpayne@68: #
jpayne@68: # The value can be given as a datetime object, as a string in the
jpayne@68: # format "yyyymmddThh:mm:ss", as a 9-item time tuple (as returned by
jpayne@68: # time.localtime()), or an integer value (as returned by time.time()).
jpayne@68: # The wrapper uses time.localtime() to convert an integer to a time
jpayne@68: # tuple.
jpayne@68: #
jpayne@68: # @param value The time, given as a datetime object, an ISO 8601 string,
jpayne@68: # a time tuple, or an integer time value.
jpayne@68:
jpayne@68:
jpayne@68: # Issue #13305: different format codes across platforms
jpayne@68: _day0 = datetime(1, 1, 1)
jpayne@68: if _day0.strftime('%Y') == '0001': # Mac OS X
jpayne@68: def _iso8601_format(value):
jpayne@68: return value.strftime("%Y%m%dT%H:%M:%S")
jpayne@68: elif _day0.strftime('%4Y') == '0001': # Linux
jpayne@68: def _iso8601_format(value):
jpayne@68: return value.strftime("%4Y%m%dT%H:%M:%S")
jpayne@68: else:
jpayne@68: def _iso8601_format(value):
jpayne@68: return value.strftime("%Y%m%dT%H:%M:%S").zfill(17)
jpayne@68: del _day0
jpayne@68:
jpayne@68:
jpayne@68: def _strftime(value):
jpayne@68: if isinstance(value, datetime):
jpayne@68: return _iso8601_format(value)
jpayne@68:
jpayne@68: if not isinstance(value, (tuple, time.struct_time)):
jpayne@68: if value == 0:
jpayne@68: value = time.time()
jpayne@68: value = time.localtime(value)
jpayne@68:
jpayne@68: return "%04d%02d%02dT%02d:%02d:%02d" % value[:6]
jpayne@68:
jpayne@68: class DateTime:
jpayne@68: """DateTime wrapper for an ISO 8601 string or time tuple or
jpayne@68: localtime integer value to generate 'dateTime.iso8601' XML-RPC
jpayne@68: value.
jpayne@68: """
jpayne@68:
jpayne@68: def __init__(self, value=0):
jpayne@68: if isinstance(value, str):
jpayne@68: self.value = value
jpayne@68: else:
jpayne@68: self.value = _strftime(value)
jpayne@68:
jpayne@68: def make_comparable(self, other):
jpayne@68: if isinstance(other, DateTime):
jpayne@68: s = self.value
jpayne@68: o = other.value
jpayne@68: elif isinstance(other, datetime):
jpayne@68: s = self.value
jpayne@68: o = _iso8601_format(other)
jpayne@68: elif isinstance(other, str):
jpayne@68: s = self.value
jpayne@68: o = other
jpayne@68: elif hasattr(other, "timetuple"):
jpayne@68: s = self.timetuple()
jpayne@68: o = other.timetuple()
jpayne@68: else:
jpayne@68: otype = (hasattr(other, "__class__")
jpayne@68: and other.__class__.__name__
jpayne@68: or type(other))
jpayne@68: raise TypeError("Can't compare %s and %s" %
jpayne@68: (self.__class__.__name__, otype))
jpayne@68: return s, o
jpayne@68:
jpayne@68: def __lt__(self, other):
jpayne@68: s, o = self.make_comparable(other)
jpayne@68: return s < o
jpayne@68:
jpayne@68: def __le__(self, other):
jpayne@68: s, o = self.make_comparable(other)
jpayne@68: return s <= o
jpayne@68:
jpayne@68: def __gt__(self, other):
jpayne@68: s, o = self.make_comparable(other)
jpayne@68: return s > o
jpayne@68:
jpayne@68: def __ge__(self, other):
jpayne@68: s, o = self.make_comparable(other)
jpayne@68: return s >= o
jpayne@68:
jpayne@68: def __eq__(self, other):
jpayne@68: s, o = self.make_comparable(other)
jpayne@68: return s == o
jpayne@68:
jpayne@68: def timetuple(self):
jpayne@68: return time.strptime(self.value, "%Y%m%dT%H:%M:%S")
jpayne@68:
jpayne@68: ##
jpayne@68: # Get date/time value.
jpayne@68: #
jpayne@68: # @return Date/time value, as an ISO 8601 string.
jpayne@68:
jpayne@68: def __str__(self):
jpayne@68: return self.value
jpayne@68:
jpayne@68: def __repr__(self):
jpayne@68: return "<%s %r at %#x>" % (self.__class__.__name__, self.value, id(self))
jpayne@68:
jpayne@68: def decode(self, data):
jpayne@68: self.value = str(data).strip()
jpayne@68:
jpayne@68: def encode(self, out):
jpayne@68: out.write("
jpayne@68: # You can create custom transports by subclassing this method, and
jpayne@68: # overriding selected methods.
jpayne@68:
jpayne@68: class Transport:
jpayne@68: """Handles an HTTP transaction to an XML-RPC server."""
jpayne@68:
jpayne@68: # client identifier (may be overridden)
jpayne@68: user_agent = "Python-xmlrpc/%s" % __version__
jpayne@68:
jpayne@68: #if true, we'll request gzip encoding
jpayne@68: accept_gzip_encoding = True
jpayne@68:
jpayne@68: # if positive, encode request using gzip if it exceeds this threshold
jpayne@68: # note that many servers will get confused, so only use it if you know
jpayne@68: # that they can decode such a request
jpayne@68: encode_threshold = None #None = don't encode
jpayne@68:
jpayne@68: def __init__(self, use_datetime=False, use_builtin_types=False,
jpayne@68: *, headers=()):
jpayne@68: self._use_datetime = use_datetime
jpayne@68: self._use_builtin_types = use_builtin_types
jpayne@68: self._connection = (None, None)
jpayne@68: self._headers = list(headers)
jpayne@68: self._extra_headers = []
jpayne@68:
jpayne@68: ##
jpayne@68: # Send a complete request, and parse the response.
jpayne@68: # Retry request if a cached connection has disconnected.
jpayne@68: #
jpayne@68: # @param host Target host.
jpayne@68: # @param handler Target PRC handler.
jpayne@68: # @param request_body XML-RPC request body.
jpayne@68: # @param verbose Debugging flag.
jpayne@68: # @return Parsed response.
jpayne@68:
jpayne@68: def request(self, host, handler, request_body, verbose=False):
jpayne@68: #retry request once if cached connection has gone cold
jpayne@68: for i in (0, 1):
jpayne@68: try:
jpayne@68: return self.single_request(host, handler, request_body, verbose)
jpayne@68: except http.client.RemoteDisconnected:
jpayne@68: if i:
jpayne@68: raise
jpayne@68: except OSError as e:
jpayne@68: if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED,
jpayne@68: errno.EPIPE):
jpayne@68: raise
jpayne@68:
jpayne@68: def single_request(self, host, handler, request_body, verbose=False):
jpayne@68: # issue XML-RPC request
jpayne@68: try:
jpayne@68: http_conn = self.send_request(host, handler, request_body, verbose)
jpayne@68: resp = http_conn.getresponse()
jpayne@68: if resp.status == 200:
jpayne@68: self.verbose = verbose
jpayne@68: return self.parse_response(resp)
jpayne@68:
jpayne@68: except Fault:
jpayne@68: raise
jpayne@68: except Exception:
jpayne@68: #All unexpected errors leave connection in
jpayne@68: # a strange state, so we clear it.
jpayne@68: self.close()
jpayne@68: raise
jpayne@68:
jpayne@68: #We got an error response.
jpayne@68: #Discard any response data and raise exception
jpayne@68: if resp.getheader("content-length", ""):
jpayne@68: resp.read()
jpayne@68: raise ProtocolError(
jpayne@68: host + handler,
jpayne@68: resp.status, resp.reason,
jpayne@68: dict(resp.getheaders())
jpayne@68: )
jpayne@68:
jpayne@68:
jpayne@68: ##
jpayne@68: # Create parser.
jpayne@68: #
jpayne@68: # @return A 2-tuple containing a parser and an unmarshaller.
jpayne@68:
jpayne@68: def getparser(self):
jpayne@68: # get parser and unmarshaller
jpayne@68: return getparser(use_datetime=self._use_datetime,
jpayne@68: use_builtin_types=self._use_builtin_types)
jpayne@68:
jpayne@68: ##
jpayne@68: # Get authorization info from host parameter
jpayne@68: # Host may be a string, or a (host, x509-dict) tuple; if a string,
jpayne@68: # it is checked for a "user:pw@host" format, and a "Basic
jpayne@68: # Authentication" header is added if appropriate.
jpayne@68: #
jpayne@68: # @param host Host descriptor (URL or (URL, x509 info) tuple).
jpayne@68: # @return A 3-tuple containing (actual host, extra headers,
jpayne@68: # x509 info). The header and x509 fields may be None.
jpayne@68:
jpayne@68: def get_host_info(self, host):
jpayne@68:
jpayne@68: x509 = {}
jpayne@68: if isinstance(host, tuple):
jpayne@68: host, x509 = host
jpayne@68:
jpayne@68: auth, host = urllib.parse._splituser(host)
jpayne@68:
jpayne@68: if auth:
jpayne@68: auth = urllib.parse.unquote_to_bytes(auth)
jpayne@68: auth = base64.encodebytes(auth).decode("utf-8")
jpayne@68: auth = "".join(auth.split()) # get rid of whitespace
jpayne@68: extra_headers = [
jpayne@68: ("Authorization", "Basic " + auth)
jpayne@68: ]
jpayne@68: else:
jpayne@68: extra_headers = []
jpayne@68:
jpayne@68: return host, extra_headers, x509
jpayne@68:
jpayne@68: ##
jpayne@68: # Connect to server.
jpayne@68: #
jpayne@68: # @param host Target host.
jpayne@68: # @return An HTTPConnection object
jpayne@68:
jpayne@68: def make_connection(self, host):
jpayne@68: #return an existing connection if possible. This allows
jpayne@68: #HTTP/1.1 keep-alive.
jpayne@68: if self._connection and host == self._connection[0]:
jpayne@68: return self._connection[1]
jpayne@68: # create a HTTP connection object from a host descriptor
jpayne@68: chost, self._extra_headers, x509 = self.get_host_info(host)
jpayne@68: self._connection = host, http.client.HTTPConnection(chost)
jpayne@68: return self._connection[1]
jpayne@68:
jpayne@68: ##
jpayne@68: # Clear any cached connection object.
jpayne@68: # Used in the event of socket errors.
jpayne@68: #
jpayne@68: def close(self):
jpayne@68: host, connection = self._connection
jpayne@68: if connection:
jpayne@68: self._connection = (None, None)
jpayne@68: connection.close()
jpayne@68:
jpayne@68: ##
jpayne@68: # Send HTTP request.
jpayne@68: #
jpayne@68: # @param host Host descriptor (URL or (URL, x509 info) tuple).
jpayne@68: # @param handler Target RPC handler (a path relative to host)
jpayne@68: # @param request_body The XML-RPC request body
jpayne@68: # @param debug Enable debugging if debug is true.
jpayne@68: # @return An HTTPConnection.
jpayne@68:
jpayne@68: def send_request(self, host, handler, request_body, debug):
jpayne@68: connection = self.make_connection(host)
jpayne@68: headers = self._headers + self._extra_headers
jpayne@68: if debug:
jpayne@68: connection.set_debuglevel(1)
jpayne@68: if self.accept_gzip_encoding and gzip:
jpayne@68: connection.putrequest("POST", handler, skip_accept_encoding=True)
jpayne@68: headers.append(("Accept-Encoding", "gzip"))
jpayne@68: else:
jpayne@68: connection.putrequest("POST", handler)
jpayne@68: headers.append(("Content-Type", "text/xml"))
jpayne@68: headers.append(("User-Agent", self.user_agent))
jpayne@68: self.send_headers(connection, headers)
jpayne@68: self.send_content(connection, request_body)
jpayne@68: return connection
jpayne@68:
jpayne@68: ##
jpayne@68: # Send request headers.
jpayne@68: # This function provides a useful hook for subclassing
jpayne@68: #
jpayne@68: # @param connection httpConnection.
jpayne@68: # @param headers list of key,value pairs for HTTP headers
jpayne@68:
jpayne@68: def send_headers(self, connection, headers):
jpayne@68: for key, val in headers:
jpayne@68: connection.putheader(key, val)
jpayne@68:
jpayne@68: ##
jpayne@68: # Send request body.
jpayne@68: # This function provides a useful hook for subclassing
jpayne@68: #
jpayne@68: # @param connection httpConnection.
jpayne@68: # @param request_body XML-RPC request body.
jpayne@68:
jpayne@68: def send_content(self, connection, request_body):
jpayne@68: #optionally encode the request
jpayne@68: if (self.encode_threshold is not None and
jpayne@68: self.encode_threshold < len(request_body) and
jpayne@68: gzip):
jpayne@68: connection.putheader("Content-Encoding", "gzip")
jpayne@68: request_body = gzip_encode(request_body)
jpayne@68:
jpayne@68: connection.putheader("Content-Length", str(len(request_body)))
jpayne@68: connection.endheaders(request_body)
jpayne@68:
jpayne@68: ##
jpayne@68: # Parse response.
jpayne@68: #
jpayne@68: # @param file Stream.
jpayne@68: # @return Response tuple and target method.
jpayne@68:
jpayne@68: def parse_response(self, response):
jpayne@68: # read response data from httpresponse, and parse it
jpayne@68: # Check for new http response object, otherwise it is a file object.
jpayne@68: if hasattr(response, 'getheader'):
jpayne@68: if response.getheader("Content-Encoding", "") == "gzip":
jpayne@68: stream = GzipDecodedResponse(response)
jpayne@68: else:
jpayne@68: stream = response
jpayne@68: else:
jpayne@68: stream = response
jpayne@68:
jpayne@68: p, u = self.getparser()
jpayne@68:
jpayne@68: while 1:
jpayne@68: data = stream.read(1024)
jpayne@68: if not data:
jpayne@68: break
jpayne@68: if self.verbose:
jpayne@68: print("body:", repr(data))
jpayne@68: p.feed(data)
jpayne@68:
jpayne@68: if stream is not response:
jpayne@68: stream.close()
jpayne@68: p.close()
jpayne@68:
jpayne@68: return u.close()
jpayne@68:
jpayne@68: ##
jpayne@68: # Standard transport class for XML-RPC over HTTPS.
jpayne@68:
jpayne@68: class SafeTransport(Transport):
jpayne@68: """Handles an HTTPS transaction to an XML-RPC server."""
jpayne@68:
jpayne@68: def __init__(self, use_datetime=False, use_builtin_types=False,
jpayne@68: *, headers=(), context=None):
jpayne@68: super().__init__(use_datetime=use_datetime,
jpayne@68: use_builtin_types=use_builtin_types,
jpayne@68: headers=headers)
jpayne@68: self.context = context
jpayne@68:
jpayne@68: # FIXME: mostly untested
jpayne@68:
jpayne@68: def make_connection(self, host):
jpayne@68: if self._connection and host == self._connection[0]:
jpayne@68: return self._connection[1]
jpayne@68:
jpayne@68: if not hasattr(http.client, "HTTPSConnection"):
jpayne@68: raise NotImplementedError(
jpayne@68: "your version of http.client doesn't support HTTPS")
jpayne@68: # create a HTTPS connection object from a host descriptor
jpayne@68: # host may be a string, or a (host, x509-dict) tuple
jpayne@68: chost, self._extra_headers, x509 = self.get_host_info(host)
jpayne@68: self._connection = host, http.client.HTTPSConnection(chost,
jpayne@68: None, context=self.context, **(x509 or {}))
jpayne@68: return self._connection[1]
jpayne@68:
jpayne@68: ##
jpayne@68: # Standard server proxy. This class establishes a virtual connection
jpayne@68: # to an XML-RPC server.
jpayne@68: #
jpayne@68: # This class is available as ServerProxy and Server. New code should
jpayne@68: # use ServerProxy, to avoid confusion.
jpayne@68: #
jpayne@68: # @def ServerProxy(uri, **options)
jpayne@68: # @param uri The connection point on the server.
jpayne@68: # @keyparam transport A transport factory, compatible with the
jpayne@68: # standard transport class.
jpayne@68: # @keyparam encoding The default encoding used for 8-bit strings
jpayne@68: # (default is UTF-8).
jpayne@68: # @keyparam verbose Use a true value to enable debugging output.
jpayne@68: # (printed to standard output).
jpayne@68: # @see Transport
jpayne@68:
jpayne@68: class ServerProxy:
jpayne@68: """uri [,options] -> a logical connection to an XML-RPC server
jpayne@68:
jpayne@68: uri is the connection point on the server, given as
jpayne@68: scheme://host/target.
jpayne@68:
jpayne@68: The standard implementation always supports the "http" scheme. If
jpayne@68: SSL socket support is available (Python 2.0), it also supports
jpayne@68: "https".
jpayne@68:
jpayne@68: If the target part and the slash preceding it are both omitted,
jpayne@68: "/RPC2" is assumed.
jpayne@68:
jpayne@68: The following options can be given as keyword arguments:
jpayne@68:
jpayne@68: transport: a transport factory
jpayne@68: encoding: the request encoding (default is UTF-8)
jpayne@68:
jpayne@68: All 8-bit strings passed to the server proxy are assumed to use
jpayne@68: the given encoding.
jpayne@68: """
jpayne@68:
jpayne@68: def __init__(self, uri, transport=None, encoding=None, verbose=False,
jpayne@68: allow_none=False, use_datetime=False, use_builtin_types=False,
jpayne@68: *, headers=(), context=None):
jpayne@68: # establish a "logical" server connection
jpayne@68:
jpayne@68: # get the url
jpayne@68: type, uri = urllib.parse._splittype(uri)
jpayne@68: if type not in ("http", "https"):
jpayne@68: raise OSError("unsupported XML-RPC protocol")
jpayne@68: self.__host, self.__handler = urllib.parse._splithost(uri)
jpayne@68: if not self.__handler:
jpayne@68: self.__handler = "/RPC2"
jpayne@68:
jpayne@68: if transport is None:
jpayne@68: if type == "https":
jpayne@68: handler = SafeTransport
jpayne@68: extra_kwargs = {"context": context}
jpayne@68: else:
jpayne@68: handler = Transport
jpayne@68: extra_kwargs = {}
jpayne@68: transport = handler(use_datetime=use_datetime,
jpayne@68: use_builtin_types=use_builtin_types,
jpayne@68: headers=headers,
jpayne@68: **extra_kwargs)
jpayne@68: self.__transport = transport
jpayne@68:
jpayne@68: self.__encoding = encoding or 'utf-8'
jpayne@68: self.__verbose = verbose
jpayne@68: self.__allow_none = allow_none
jpayne@68:
jpayne@68: def __close(self):
jpayne@68: self.__transport.close()
jpayne@68:
jpayne@68: def __request(self, methodname, params):
jpayne@68: # call a method on the remote server
jpayne@68:
jpayne@68: request = dumps(params, methodname, encoding=self.__encoding,
jpayne@68: allow_none=self.__allow_none).encode(self.__encoding, 'xmlcharrefreplace')
jpayne@68:
jpayne@68: response = self.__transport.request(
jpayne@68: self.__host,
jpayne@68: self.__handler,
jpayne@68: request,
jpayne@68: verbose=self.__verbose
jpayne@68: )
jpayne@68:
jpayne@68: if len(response) == 1:
jpayne@68: response = response[0]
jpayne@68:
jpayne@68: return response
jpayne@68:
jpayne@68: def __repr__(self):
jpayne@68: return (
jpayne@68: "<%s for %s%s>" %
jpayne@68: (self.__class__.__name__, self.__host, self.__handler)
jpayne@68: )
jpayne@68:
jpayne@68: def __getattr__(self, name):
jpayne@68: # magic method dispatcher
jpayne@68: return _Method(self.__request, name)
jpayne@68:
jpayne@68: # note: to call a remote object with a non-standard name, use
jpayne@68: # result getattr(server, "strange-python-name")(args)
jpayne@68:
jpayne@68: def __call__(self, attr):
jpayne@68: """A workaround to get special attributes on the ServerProxy
jpayne@68: without interfering with the magic __getattr__
jpayne@68: """
jpayne@68: if attr == "close":
jpayne@68: return self.__close
jpayne@68: elif attr == "transport":
jpayne@68: return self.__transport
jpayne@68: raise AttributeError("Attribute %r not found" % (attr,))
jpayne@68:
jpayne@68: def __enter__(self):
jpayne@68: return self
jpayne@68:
jpayne@68: def __exit__(self, *args):
jpayne@68: self.__close()
jpayne@68:
jpayne@68: # compatibility
jpayne@68:
jpayne@68: Server = ServerProxy
jpayne@68:
jpayne@68: # --------------------------------------------------------------------
jpayne@68: # test code
jpayne@68:
jpayne@68: if __name__ == "__main__":
jpayne@68:
jpayne@68: # simple test program (from the XML-RPC specification)
jpayne@68:
jpayne@68: # local server, available from Lib/xmlrpc/server.py
jpayne@68: server = ServerProxy("http://localhost:8000")
jpayne@68:
jpayne@68: try:
jpayne@68: print(server.currentTime.getCurrentTime())
jpayne@68: except Error as v:
jpayne@68: print("ERROR", v)
jpayne@68:
jpayne@68: multi = MultiCall(server)
jpayne@68: multi.getData()
jpayne@68: multi.pow(2,9)
jpayne@68: multi.add(1,2)
jpayne@68: try:
jpayne@68: for response in multi():
jpayne@68: print(response)
jpayne@68: except Error as v:
jpayne@68: print("ERROR", v)