Intro to Lua and Openresty, Part 7: Limit HTTP Methods

Posted on March 7, 2017

In Part 6 of this series, we demonstrated how to write a simple but effective data processing sink with redis as the queue to safely store message before and during processing.

In this post, we’ll take a short detour to briefly investigate conditionals based on the HTTP method used in the request.

If you are following along in the code, we are here.

Restricting access based on HTTP method

Let’s say you have an endpoint that accepts GET and POST, how do you ensure you only process requests of those type, and block non-allowed methods as early as possible? The purpose of this exercise is to demonstrate how to inspect and react to the specific HTTP method used to access the URI location. While there are multiple ways to accomplish this directly in nginx.conf, we will use Lua to inspect and take action on these methods.

Micro Example

local http_method = ngx.var.request_method
if http_method == ngx.HTTP_GET then
  local cjson = require "cjson"
  ngx.status = ngx.HTTP_OK
  ngx.say(cjson.encode({method = "GET", status = "allowed"}))
  return ngx.exit(ngx.HTTP_OK)
else
  ngx.status = ngx.HTTP_NOT_ALLOWED
  ngx.say(cjson.encode({method = http_method , status = "denied"}))
  return ngx.exit(ngx.HTTP_NOT_ALLOWED)
end

Note.. in testing this, ngx.HTTP_GET appears to be 2, while ngx.HTTP_POST is 8, so I have used this instead:

local http_method = ngx.var.request_method
if http_method == "GET" then
  local cjson = require "cjson"
  ngx.status = ngx.HTTP_OK
  ngx.say(cjson.encode({method = "GET", status = "allowed"}))
  return ngx.exit(ngx.HTTP_OK)
else
  ngx.status = ngx.HTTP_NOT_ALLOWED
  ngx.say(cjson.encode({method = http_method , status = "denied"}))
  return ngx.exit(ngx.HTTP_NOT_ALLOWED)
end

Put it into a webapp

With some initial tests using the snippet above, we settle on the following:

worker_processes  1;
error_log error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen        8000;
        charset       utf-8;
        charset_types application/json;
        default_type  application/json;
        location /get {
            content_by_lua '
              local cjson = require "cjson"
              local http_method = ngx.var.request_method
              if http_method == "GET" then
                ngx.status = ngx.HTTP_OK
                ngx.say(cjson.encode({method = "GET", status = "allowed"}))
                return ngx.exit(ngx.HTTP_OK)
              else
                ngx.status = ngx.HTTP_NOT_ALLOWED
                ngx.say(cjson.encode({method = http_method , status = "denied"}))
                return ngx.exit(ngx.HTTP_NOT_ALLOWED)
              end
            ';
        }
        location /post {
            content_by_lua '
              local cjson = require "cjson"
              local http_method = ngx.var.request_method
              if http_method == "POST" then
                ngx.status = ngx.HTTP_OK
                ngx.say(cjson.encode({method = "POST", status = "allowed"}))
                return ngx.exit(ngx.HTTP_OK)
              else
                ngx.status = ngx.HTTP_NOT_ALLOWED
                ngx.say(cjson.encode({method = http_method , status = "denied"}))
                return ngx.exit(ngx.HTTP_NOT_ALLOWED)
              end
            ';
        }
    }
}

Here is the Dockerfile:

FROM openresty/openresty:alpine-fat

EXPOSE 8000
RUN /usr/local/openresty/luajit/bin/luarocks install lua-cjson
ADD nginx.conf /usr/local/openresty/nginx/conf/nginx.conf

The Makefile:

build:
        docker build --tag=app:8 --rm=true ./

run:
        docker run -d --name app --net host -p 127.0.0.1:8000:8000 app:8

dev:
        docker run --rm -it --name app --net host --entrypoint /bin/sh -v `pwd`/app:/src app:8

clean:
        docker stop app || true
        docker rm   app || true

reload:
        docker exec -it app /usr/local/openresty/nginx/sbin/nginx -s reload

logs:
        docker exec -it app tail -f /usr/local/openresty/nginx/error.log

test:
        curl -i -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' localhost:8000/get
        curl -i -H "Content-Type: application/json"                                                  localhost:8000/get
        curl -i -H "Content-Type: application/json"                                                  localhost:8000/post
        curl -i -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' localhost:8000/post

Build the Image

ᐅ make build
docker build --tag=app:8 --rm=true ./
Sending build context to Docker daemon 6.144 kB
Step 1 : FROM openresty/openresty:alpine-fat
 ---> 366babf2b04d
Step 2 : EXPOSE 8000
 ---> Using cache
 ---> 35a8c6e42825
Step 3 : RUN /usr/local/openresty/luajit/bin/luarocks install lua-cjson
 ---> Using cache
 ---> 88eaefcb0701
Step 4 : ADD nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
 ---> 0f06fa265a56
Removing intermediate container 17a6406417d9
Successfully built 0f06fa265a56

Run the image

ᐅ make run
docker run -d --name app --net host -p 127.0.0.1:8000:8000 app:8
2f52d4449835fe5bc3cfe3881e45c442d742bf1bfbd784d201e1ef3872615a5d

Run tests on the webapp

ᐅ make test
curl -i -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' localhost:8000/get
HTTP/1.1 405 Not Allowed
Server: openresty/1.11.2.2
Date: Sun, 05 Mar 2017 05:22:31 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"status":"denied","method":"POST"}
curl -i -H "Content-Type: application/json"                                                  localhost:8000/get
HTTP/1.1 200 OK
Server: openresty/1.11.2.2
Date: Sun, 05 Mar 2017 05:22:31 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"status":"allowed","method":"GET"}
curl -i -H "Content-Type: application/json"                                                  localhost:8000/post
HTTP/1.1 405 Not Allowed
Server: openresty/1.11.2.2
Date: Sun, 05 Mar 2017 05:22:31 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"status":"denied","method":"GET"}
curl -i -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' localhost:8000/post
HTTP/1.1 200 OK
Server: openresty/1.11.2.2
Date: Sun, 05 Mar 2017 05:22:31 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"status":"allowed","method":"POST"}

Continuing on to Part 8, Reading from the Postgres.