Programmable
API Gateway
with a touch of moonlight
What can your reverse-proxy server do?
Marcin Stożek "Perk"
API Gateway
Glossary
Very short LUA intro
- Arrays are indexed from 1
- Strings concatenation with ..
- Null with nil
- Non equal with ~=
- Comment with --
- multiline --[[ comment here ]]
- Logical operators: and, or, not
- Every variable and function is global
- use local keyword (a lot)
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
demo
hello-world
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...
demo
correlation-id
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()
}
}
demo
http-session
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;
}
Global variables
Use of global variables in your Lua code, eg. share the db connection
Global variables
- you can use global variables
- but only for worker
- but only for worker
- make sure your db lib is thread-safe
demo
global-variables
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.
demo
rotate-image
Web
Application
Firewall
(or just 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
OpenResty with plugins, no Lua knowledge needed*
* for happy paths at least ;)
Auth and correlation id
- Everything can be set via... HTTP API
- commercial and open source UIs available
Auth and correlation id
curl -X POST --url http://localhost:8001/services/app-service/plugins
--data "name=correlation-id"
--data "config.header_name=correlation-id"
--data "config.generator=uuid#counter"
--data "config.echo_downstream=false"
curl -X POST http://localhost:8001/services/app-service/plugins
--data "name=key-auth"
--data "config.hide_credentials=false"
curl -X POST --url http://localhost:8001/services/
--data 'name=app-service'
--data 'url=http://app'
Is it fast?
Yes :)
but it depends of course...
here's some benchmark results
demo
speed-test
Links, lynx, wget, w3m, curl...
print("Thank you!")
Programmable API Gateway with a touch of moonlight
By Marcin Stożek
Programmable API Gateway with a touch of moonlight
What can your reverse-proxy server do?
- 2,795