staticshin

Routing in openresty

Nginx offers very proficient routing mechanisms in the form of location blocks. But since it is mostly used as a proxy server the location directives remain underutilized as simple endpoints for maybe passing on the requests to another webserver,rewriting urls or for redirecting http requests. Openresty turns nginx into a programable webserver and therefore gives us a better opportunity to fully utilize the capabilities of location block directives and create complex routing schemes for our applications.

In any webserver there are a few basic routing features that we take for granted:-

  • That we should be able to parametrize the urls
  • That we should be able to accept/reject a request based on it's method
  • That we should be able to "inject" certain routines that allow us perform housekeeping tasks on an endpoint. eg: checking the validity of the cookie before accepting a request.

All of these features can be implemented quite easily in openresty with a mix of default nginx directives and some simple lua code.

Should hats have heads in them?

First let us see how we can parametrize the urls. Consider this simple location block

                
location ~*/a/(?<param>[a-zA-Z]+){
content_by_lua 'ngx.say("first match")';
}

		
              

The syntax it very simple to understand. `~*` at the beginning of the location string tells us to look for a case insensitive regex pattern. `(?<param>[a-zA-Z0-9]+)` captures a match in a named group called `param`. The match occurs if the character class [a-zA-Z0-9]+ is satisfied. If the url passes the regex in this location block you should see the string "first match" printed on the screen.

               
curl http://localhost:3125/a/b
first match

curl http://localhost:3125/a/baba
first match

	      
             

Lets add another location block to make things a bit more interesting.

                
location ~*/(?<param>[a-zA-Z0-9]+)/a{
content_by_lua 'ngx.say("second match")';
}
		
              

The behaviour that we are expecting now is any url pattern that starts with 'a' should match the first location. And any url pattern that ends with "a" should match the second string.

                
curl http://localhost:3125/b/a/
second match
b
curl http://localhost:3125/a/b/
first match
b

curl http://localhost:3125/b/a/c
second match
b

		

              

As we can see the results are as expected. You can add rewrites to reject any string after the regex has been satisfied. For example the following rewrite in the first location block will not consider any string after the match

                
rewrite ~*/a/(?<param>[a-zA-Z0-9]+)/(.+) /a/$1/	 permanent; 
              

What this does is to rewrite all the requests of type /a/b/c to /a/b/ . So that nginx doesn't get carried away when matching regex locations.

Do walls ruin wallpapers?

Next let us look at limiting the methods only to a particular type of request. In nginx this is as easy as a thing can be.

                

location = /return_to_sender{
limit_except POST{}
return 200;
}
		
              

This location block only accepts post requests and denies everything else. Lets test it out:-

                
curl -v -X PUT http://localhost:3125/return_to_sender
< HTTP/1.1 405 Not Allowed
< Server: openresty
< Date: Sat, 08 Oct 2016 17:39:33 GMT
< Content-Type: text/html
< Content-Length: 399
< Connection: keep-alive
< ETag: "57f8c5e9-18f"

curl -v -X DELETE http://localhost:3125/return_to_sender

< HTTP/1.1 405 Not Allowed
< Server: openresty
< Date: Sat, 08 Oct 2016 17:41:19 GMT
< Content-Type: text/html
< Content-Length: 399
< Connection: keep-alive
< ETag: "57f8c5e9-18f"
<     

curl -v -X POST http://localhost:3125/return_to_sender
< HTTP/1.1 200 OK
< Server: openresty
< Date: Sat, 08 Oct 2016 17:41:53 GMT
< Content-Type: text/plain
< Content-Length: 0
< Connection: keep-alive
< X-Frame-Options: SAMEORIGIN
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block   
   
              

You can also supply more than one method to accept like so:-

                

location = /elvis{
limit_except POST PUT{}
return 200;
}		  
		
              

The location block above will only accept POST and PUT methods and deny everything else.

It is generally a good practice to explicitly specify what methods a location will except. Saves unwanted trouble later on.

Is bread better when eaten?

One of the features of modern routing frameworks is that it allows us to run functions before the processing is passed on to the actual handler. Openresty provides a good approximation for this in the form of access_by_lua handlers. In practice this is very simple. Consider that you want to to give an authenticated access to an endpoint. Here's a code snippet

                

location =  /auth {
access_by_lua_block {	      
local headers = ngx.req.get_headers()
local uname_pass = 'abc:def'
if  headers["Authorization"] ~= "Basic "..ngx.encode_base64(uname_pass) then
   ngx.header["WWW-Authenticate"]="Basic"
   ngx.req.clear_header("Authorization")
   return  ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
ngx.exit(ngx.OK)
 }
 return 200;
}		  
		
              

Any request to /auth without the valid authentication header will be rejected, requests with a valid auth header will be accepted. Now if you create a lua file out of this code you can simply reference the same file in as many location directives you want.This is not exactly a catch all function that most other frameworks provide but it's a pretty close enough approximation in practical terms.

Is cleanliness good for the soap?

There are certain principles that have served me well when creating routes for openresty applications:-

  1. If you are parametrizing the urls try to keep the name of the capturing group same. For example you can name all parameters as `param` regardless of the position they appear in the location block. Also try to limit every url to have no more than one parameter. If other parameters are needed they can always be passed in the body. The advantage is that you get a bit of consistency in your locations. This will help a great deal when you want to run validation checks on your parameters as you know that if a url has a parameter it will be named param. With a consistent name you can write a series of checks once and run them against all the url parameters.
  2. Remember to use ngx.ctx. Often you'll find yourself in a situation where you are validating post parameters, url arguments etc. Once they've been verified you can save them inside a ngx.ctx.VAR. Since the ngx.ctx remains valid for the duration of the request you can gain some benefits by variable caching. Good candidates for caching are post body paramters, decoded json, user meta data etc Think of ngx.ctx as a per request cache and use it accordingly.
  3. Do not disregard nginx's directives in favour of lua.If you want to rewrite/redirect add or remove certain headers, impose some accessibility restrictions or do anything else, research whether its possible to do it with a built in directive or a configuration parameter. Keep things tight, use lua code only when you have to.
  4. Make your routes as exclusive as possible. Lacking a header? Bad request. Illegal method? 405. You can create a general "location_rules.conf" file and then include it in the location blocks to DRY up the code. With clever use of include you can even create very elaborate access control lists consisting of nothing more than a few basic nginx configrations.
  5. A single nginx instance can have multiple server blocks. Effectively use them to create separate instances of services when it makes sense. Openresty gives you tools that encourages a micro service architecture.And since you only need to change a few lines of configuration to move back and forth between a microservice and a unified server design experimentation is not all that expensive.

All code is available for testing on the github repository

-Akshat Jiwan Sharma

comments powered by Disqus