Nginx + Lua
=
OpenResty

What can your web server do?

Marcin Stożek "Perk"

Glossary

  • NGINX
    • web server, reverse proxy, load balancer
       
  • Lua
    • programming language, AD 1993
       
  • OpenResty
    • nginx with Lua module configured

Hello world!

worker_processes  1;

error_log  logs/error.log  info;

events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  localhost;
        default_type text/html;

        location / {
            root /data/www;
        }
    }
}
worker_processes  1;

error_log  logs/error.log  info;

events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  localhost;
        default_type text/html;

        location / {
            content_by_lua_block {
                ngx.say("Hello world!")
            }
        }
    }
}

NGINX

OpenResty

Directives

  • *_by_lua_block
  • *_by_lua_file

* [init, init_worker], [set, rewrite, access, ssl_certificate], [content, balancer, header, body_filter], [log]

content_by_lua_file

header_by_lua_block

Correlation ID

Create correlation id as fast as request reaches our server, pass it to every subsequent request inside our network.

Don't let this correlation id leak outside.

# proxy:/nginx.conf


location / {

  set_by_lua $correlation_id '
    local correlation_id = string.format("%010d", math.random(0, 10000000000))
    ngx.log(ngx.INFO, "Created correlation-id: " .. correlation_id)
  
    return correlation_id
  ';


  proxy_set_header Correlation-ID $correlation_id;


  proxy_pass http://app-server;


  header_filter_by_lua_block {
    ngx.var.app_correlation_id =
      ngx.resp.get_headers()["App-Correlation-ID"] or "something-is-wrong-id"

    ngx.header.content_length = nil
    ngx.header.app_correlation_id = nil
  }


  body_filter_by_lua '
    ngx.arg[1] = string.gsub(ngx.arg[1], "placeholder", ngx.var.correlation_id)
  ';

}

# app - localhost:8080

$ curl --header "correlation-id: 0xCAFEBABE" -v localhost:8080

> correlation-id: 0xCAFEBABE
...
< App-Correlation-ID: 0xCAFEBABE
...
<!DOCTYPE html>...

# proxy - localhost:80

$ curl --header "correlation-id: 0xCAFEBABE" -v localhost:80

> correlation-id: 0xCAFEBABE
...
(no App-Correlation-ID in here)
...
<!DOCTYPE html>
sent correlation-id: 812312...
received app-correlation-id: 812312...

HTTP Session

Manage HTTP session by the proxy server

$ luarocks install lua-resty-session

location /create {
  content_by_lua_block {
    local session = require "resty.session".start()
    session.data.name = "OpenResty Fan"
    session:save()
  }
}


location /check {
  content_by_lua_block {
    local session = require "resty.session".open()
    ngx.say("Hello " .. session.data.name or "Anonymous")
  }
}


location /destroy {
  content_by_lua_block {
    local session = require "resty.session".start()
    session:destroy()
  }
}

Dynamic routing

Route to dynamic hosts based on
Redis / Eureka / custom file / whatever

driven by user-agent

location / { 
  resolver 127.0.0.11; # Docker's embedded DNS server

  set $target ''; 
  access_by_lua_block { 
    local key = ngx.var.http_user_agent

    local redis = require "resty.redis"
    local red = redis:new()

    red:set_timeout(1000) -- 1 second

    local ok, err = red:connect("redis", 6379)

    local host, err = red:get(key)

    ngx.var.target = host
  }

  proxy_pass http://$target;
}

Fun with images

Dynamic images resize with caching and DOS protection.

$ luarocks install magick
$ luarocks install luafilesystem

server {
  listen 80; 

  location @image_server {
    content_by_lua_file "image_server.lua";
  }

  location ~ ^/images/(?<sig>[^/]+)/(?<size>[^/]+)/(?<path>.*\.(?<ext>[a-z_]*))$ {
    root cache;
    set_md5 $digest "$size/$path";
    try_files /$digest.$ext @image_server;
  }

  location / { 
    content_by_lua_file "show_links.lua";
  }
}

file:/image_server.lua

local function return_not_found(msg)
  ngx.status = ngx.HTTP_NOT_FOUND
  ngx.header["Content-type"] = "text/html"
  ngx.say(msg or "not found")
  ngx.exit(0)
end

local function calculate_signature(str)
  return ngx.encode_base64(ngx.hmac_sha1(secret, str))
    :gsub("[+/=]", {["+"] = "-", ["/"] = "_", ["="] = ","})
    :sub(1,12)
end

local signature = calculate_signature(size .. "/" .. path)
if signature ~= sig then
  return_not_found("invalid signature, valid one is: " .. signature)
end

local source_fname = images_dir .. path

-- make sure the file exists
local file = io.open(source_fname)

if not file then
  return_not_found("file " .. path .. " not found")
end

file:close()

file:/show_links.lua

local function values(t)
  local i = 0
  return function() i = i + 1; return t[i] end
end

local function create_link(res, file)
  local sig = calculate_signature(res .. "/" .. file)
  local href="http://localhost/images/" .. sig .. "/" .. res .. "/" .. file

  html_result = html_result .. "<a href='" .. href .. "'>" .. file .. ")</a>"
end

local resolutions = {"1280x1024", "1024x768", "800x600", "640x480"}
local lfs = require "lfs"

for file in lfs.dir(images_dir) do
  local attr = lfs.attributes(images_dir .. "/" .. file)
  if attr.mode == "file" then
    for res in values(resolutions) do
      create_link(res, file)
      create_link("nz-" .. res, file)
    end
  end
end

User story:

As a user from New Zealand
I want my images rotated
so that I don't need to rotate my phone.

Web
Application
Firewall

(so called WAF)

WAF - is it good?

  • deployed as reverse proxies
  • active defense against known vulnerabilities
  • webapp source code not available
    • changes on-the-fly
  • honey pot
  • no changes in webapp needed

CloudFlare uses WAF written in Lua an run on top of NGINX...
sounds familiar?

Is it fast?

Yes :)

demo
speed-test

Links, lynx, w3m, curl...

Questions?

Nginx & Lua FTW!

Nginx + Lua = OpenResty

By Marcin Stożek

Nginx + Lua = OpenResty

What can your web server do?

  • 109
Loading comments...

More from Marcin Stożek