Cookies and XML-RPC

You're using the xmlrpclib stuff to access Bugzilla over XML-RPC. But there's a problem. Bugzilla requires you to log in first, and then sets a login cookie which you must return on all subsequent operations.

The solution is to create a Transport that collects cookies set by the server and returns them on all subsequent operations.

But xmlrpclib.Transport does not have any interface hooks that let you do that. Yes, you could write your own Transport class, but that will spend most of its time duplicating the functionality in xmlrpclib.Transport, which isn't good. Oh, and to capture the cookies you need to examine the response from the server, and there's no way to do that across versions 2.4-2.7 of Python without replacing the entire request() method. Which is in turn tricky because of the internal changes between those Python versions.

Oh, and you also want to have HTTP and HTTPS transports, but without repeating yourself.

Here's my solution. It uses multiple inheritance to generate transport classes without repeating the request() implementation, which is in turn carefully crafted to work across Pythons 2.4-2.7 inclusive.

If only xmlrpclib.Transport had had some better hooks, this could have been a lot easier.

class cookietransportrequest:
    """A Transport request method that retains cookies over its lifetime.
 
    The regular xmlrpclib transports ignore cookies. Which causes
    a bit of a problem when you need a cookie-based login, as with
    the Bugzilla XMLRPC interface.
 
    So this is a helper for defining a Transport which looks for
    cookies being set in responses and saves them to add to all future
    requests.
    """
 
    # Inspiration drawn from
    # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
    # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
    #
    # Note this must be an old-style class so that __init__ handling works
    # correctly with the old-style Transport class. If you make this class
    # a new-style class, Transport.__init__() won't be called.
 
    cookies = []
    def send_cookies(self, connection):
        if self.cookies:
            for cookie in self.cookies:
                connection.putheader("Cookie", cookie)
 
    def request(self, host, handler, request_body, verbose=0):
        self.verbose = verbose
 
        # issue XML-RPC request
        h = self.make_connection(host)
        if verbose:
            h.set_debuglevel(1)
 
        self.send_request(h, handler, request_body)
        self.send_host(h, host)
        self.send_cookies(h)
        self.send_user_agent(h)
        self.send_content(h, request_body)
 
        # Deal with differences between Python 2.4-2.6 and 2.7.
        # In the former h is a HTTP(S). In the latter it's a
        # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
        # HTTP(S) has an underlying HTTP(S)Connection, so extract
        # that and use it.
        try:
            response = h.getresponse()
        except AttributeError:
            response = h._conn.getresponse()
 
        # Add any cookie definitions to our list.
        for header in response.msg.getallmatchingheaders("Set-Cookie"):
            val = header.split(": ", 1)[1]
            cookie = val.split(";", 1)[0]
            self.cookies.append(cookie)
 
        if response.status != 200:
            raise xmlrpclib.ProtocolError(host + handler, response.status,
                                          response.reason, response.msg.headers)
 
        payload = response.read()
        parser, unmarshaller = self.getparser()
        parser.feed(payload)
        parser.close()
 
        return unmarshaller.close()
 
class cookietransport(cookietransportrequest, xmlrpclib.Transport):
    pass
 
class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
    pass

Now choose the appropriate transport:

    proxy = xmlrpclib.ServerProxy(url, transport(url))
    proxy.User.login(dict(login=user, password=passwd))
 
    def transport(self, uri):
        """Return an appropriate Transport for the URI.
 
        If the URI type is https, return a CookieSafeTransport.
        If the type is http, return a CookieTransport.
        """
        if urlparse.urlparse(uri, "http")[0] == "https":
            return cookiesafetransport()
        else:
            return cookietransport()