Wednesday, March 27th, 2013

Layering SSL over the XMLRPCServer

I really enjoy how simple it is to use xmlrpclib over other APIs such as REST. I also understand that it's insecure, as it does not support authentication as REST APIs do. Although there should technically be a way to implement an authentication layer over XML-RPC. The SimpleXMLRPCServer class which ships in Python's standard library doesn't natively support SSL socket encryption. For private XML-RPC Services, which I do not want otherwise exposed, especially over plain text, SSL encryption is required. I also prefer a stand-alone server over piggybacking on an SSL enabled web-server. This enables me to choose a random port, and create services which can otherwise monitor the web service remotely. How can you monitor a web service using a web service, if the web service is having trouble... Also since the xmlrpclib module ships in the Python standard library, and supports SSL/HTTPS servers, it makes it a great light-weight protocol to use anywhere Python can be installed. I have Python installed on my Symbian smartphone, and both my Android phone and tablet. Running a quick script from my home screen yields quick results from my XML-RPC service. Anyways, enough of my rambling and justifications for using XML-RPC over a more popular protocol... Here's the class you came here to steal from my blog:

from SocketServer import TCPServer
import ssl
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCDispatcher, SimpleXMLRPCRequestHandler
try:
    import fcntl
except ImportError:
    fcntl = None

CERT_FILE = '/home/kveroneau/cert.pem'

class SSLServer(TCPServer):
    def get_request(self):
        newsocket, fromaddr = self.socket.accept()
        connstream = ssl.wrap_socket(newsocket, server_side=True,
                                     certfile=CERT_FILE, keyfile=CERT_FILE,
                                     ssl_version=ssl.PROTOCOL_SSLv23)
        return (connstream, fromaddr)

class SimpleXMLRPCServer(SSLServer,
                         SimpleXMLRPCDispatcher):
    """Simple XML-RPC server.

    Simple XML-RPC server that allows functions and a single instance
    to be installed to handle requests. The default implementation
    attempts to dispatch XML-RPC calls to the functions or instance
    installed in the server. Override the _dispatch method inhereted
    from SimpleXMLRPCDispatcher to change this behavior.
    """

    allow_reuse_address = True

    # Warning: this is for debugging purposes only! Never set this to True in
    # production code, as will be sending out sensitive information (exception
    # and stack trace details) when exceptions are raised inside
    # SimpleXMLRPCRequestHandler.do_POST
    _send_traceback_header = False

    def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
                 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
        self.logRequests = logRequests

        SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
        SSLServer.__init__(self, addr, requestHandler, bind_and_activate)

        # [Bug #1222790] If possible, set close-on-exec flag; if a
        # method spawns a subprocess, the subprocess shouldn't have
        # the listening socket open.
        if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
            flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
            flags |= fcntl.FD_CLOEXEC
            fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)

if __name__ == '__main__':
    print 'Running XML-RPC server on port 8000'
    server = SimpleXMLRPCServer(("localhost", 8000))
    server.register_function(pow)
    server.register_function(lambda x,y: x+y, 'add')
    server.serve_forever()

There must be a more elegant way of doing this, but I was unsuccessful with creating a Mixin that could simply overwrite TCPServer.get_request(). If you know of a better way of implementing this code, please send it my way, and I will update this post and credit you for your assistance. To generate the PEM file mentioned above, use this command-line:

kveroneau@sys1:~$ openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout cert.pem
Generating a 1024 bit RSA private key
...++++++
..................................++++++
writing new private key to 'cert.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CA
State or Province Name (full name) [Some-State]:Manitoba
Locality Name (eg, city) []:Winnipeg
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Veroneau.net
Organizational Unit Name (eg, section) []:I.T.
Common Name (eg, YOUR name) []:Kevin
Email Address []:***HIDDEN***

A bonus of this, is that you can use SSLServer class to replace any TCPServer class and add SSL to any existing program that uses TCPServer. For non-light weight programs, consider using Twisted, it is a much more featureful Python package for creating and deploying networked applications.

Python Powered | © 2012-2014 Kevin Veroneau