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.
importbase64importloggingimportlogging.configimporttornado.httpserverimporttornado.ioloopimporttornado.weblog=logging.getLogger("root")defauthenticator(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 """ifhandle==password:return(handle,'User Name')returnNonedefuser_extractor(user_data):""" This method extracts the user handle from the data structure returned by the authenticator """returnuser_datadefbasic_authenticate(realm,authenticator,user_extractor):""" This is a basic authentication interceptor which protects the desired URIs and requires authentication as per configuration """defwrapper(self,transforms,*args,**kwargs):def_request_basic_auth(self):ifself._headers_written:raiseException('headers have already been written')self.set_status(401)self.set_header('WWW-Authenticate','Basic realm="%s"'%realm)self.finish()returnFalserequest=self.requestformat=''clazz=self.__class__log.debug('intercepting for class : %s',clazz)try:auth_hdr=request.headers.get('Authorization')ifauth_hdr==None:return_request_basic_auth(self)ifnotauth_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)ifuser_info:self._user_info=user_infoself._current_user=user_extractor(user_info)log.debug('authenticated user is : %s',str(self._user_info))else:return_request_basic_auth(self)exceptException,e:return_request_basic_auth(self)returnTruereturnwrapperdefinterceptor(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 """defclasswrapper(cls):defwrapper(old):definner(self,transforms,*args,**kwargs):log.debug('Invoking wrapper %s',func)ret=func(self,transforms,*args,**kwargs)ifret:returnold(self,transforms,*args,**kwargs)else:returnretreturninnercls._execute=wrapper(cls._execute)returnclsreturnclasswrapper@interceptor(basic_authenticate('dummy_realm',authenticator,user_extractor))classMainHandler(tornado.web.RequestHandler):defget(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()