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()