Fork me on GitHub

Implementing request interceptors for tornado

One of my favourite new software introductions last year was the Tornado web server. However ever since my Webwork 2 (and subsequently Struts 2) days, I had learnt to greatly appreciate the power and cleanliness of implementing interceptors for a variety of aspects related to request preprocessing. The same feature is also available on a variety of wsgi based web application frameworks except that these are referred to as wsgi middleware.

So it was with great disappointment that I realised Tornado simply did not have any such interceptors. Worse, there was no way to plug in or roll one's own, since the tornado request processing pipeline had no place where one could plug something in. In a statically typed language it would have required me to take one of the two options - (a) fork Tornado codebase, change it to introduce plugins or (b) live without it. Well every once in a while in a dynamic typed language you run into a situation where metaprogramming saves your bacon. It was relatively easy to implement one for Tornado.

So how does one do it. Tornado during request processing calls a method called _execute on the handler (which is always available on all handlers since its in the base class for handlers). I wrote a class decorator called interceptor which wraps a class, and in doing so actually wraps one of its methods. It saves the reference to the current _execute method, replaces it with a call to another user supplied method (as a part of applying the interceptor), and calls the saved _execute method reference after the user supplied method returns successfully. Note that as per the semantics, if the user supplied method returns False, further request processing is aborted. It is also possible to chain a number of such interceptors.

In the example below, I have taken the basic authentication logic found in one of the Tornado examples and reformatted the same as an interceptor. The first two functions authenticator and user_extractor are rather trivialised implementations of the user supplied authentication logic.

So here's the complete sample tornadoweb application with the MainHandler being wrapped with an interceptors which triggers basic authentication. Enjoy.

import base64
import logging
import logging.config
import tornado.httpserver
import tornado.ioloop
import tornado.web

log = logging.getLogger("root")

def authenticator(realm,handle,password):
    """ 
    This method is a sample authenticator. 
    It treats authentication as successful
    if the handle and passwords are the same.
    It returns a tuple of handle and user name 
    """
    if handle == password :
        return (handle,'User Name')
    return None

def user_extractor(user_data):
    """
    This method extracts the user handle from
    the data structure returned by the authenticator
    """
    return user_data[0]

def basic_authenticate(realm, authenticator,user_extractor) :
    """
    This is a basic authentication interceptor which 
    protects the desired URIs and requires 
    authentication as per configuration
    """
    def wrapper(self, transforms, *args, **kwargs):
        def _request_basic_auth(self):
            if self._headers_written: 
                raise Exception('headers have already been written')

            self.set_status(401)
            self.set_header('WWW-Authenticate','Basic realm="%s"' % realm)
            self.finish()

            return False
        request = self.request
        format = ''
        clazz = self.__class__
        log.debug('intercepting for class : %s', clazz)
        try:
            auth_hdr = request.headers.get('Authorization')

            if auth_hdr == None: 
                return _request_basic_auth(self)
            if not auth_hdr.startswith('Basic '): 
                return _request_basic_auth(self)

            auth_decoded = base64.decodestring(auth_hdr[6:])
            username, password = auth_decoded.split(':', 2)

            user_info = authenticator(realm, unicode(username), password)
            if user_info :
                self._user_info = user_info
                self._current_user = user_extractor(user_info)
                log.debug('authenticated user is : %s', 
                          str(self._user_info))
            else:
                return _request_basic_auth(self)
        except Exception, e:
            return _request_basic_auth(self)
        return True
    return wrapper

def interceptor(func):
    """
    This is a class decorator which is helpful in configuring
    one or more interceptors which are able to intercept, inspect,
    process and approve or reject further processing of the request
    """
    def classwrapper(cls):
        def wrapper(old):
            def inner(self, transforms, *args, **kwargs):
                log.debug('Invoking wrapper %s',func)
                ret = func(self,transforms,*args,**kwargs)
                if ret :   
                    return old(self,transforms,*args,**kwargs)
                else :
                    return ret
            return inner
        cls._execute = wrapper(cls._execute)
        return cls
    return classwrapper

@interceptor(basic_authenticate('dummy_realm',authenticator,user_extractor))
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

application = tornado.web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Comments !

social