Haproxyサーバーには、Luaスクリプトを実行するための組み込み機能があります。
Luaプログラミング言語は、さまざまなサーバーの機能を拡張するために広く使用されています。たとえば、Luaは、Redis、Nginx(nginx-extras、openresty)、Envoyサーバー用にプログラムできます。Luaプログラミング言語は、スクリプト言語としてアプリケーションに簡単に組み込むために開発されたばかりなので、これは非常に自然なことです。
この投稿では、LuaがHaproxyの機能を拡張するためのユースケースについて説明します。
ドキュメントによると、Haproxyサーバー上のLuaスクリプトは、次の6つのコンテキストで実行できます。
- 本文コンテキスト(Haproxyサーバー構成をロードするとき、lua-loadディレクティブで指定されたスクリプトが実行されるときのコンテキスト);
- initコンテキスト(構成のロード直後に呼び出され、システム関数core.register_init(関数)に登録される関数のコンテキスト);
- タスクコンテキスト(core.register_task(function)システム関数によって登録されたスケジュールされた関数のコンテキスト);
- アクションコンテキスト(システム関数core.register_action(function)によって登録された関数のコンテキスト);
- サンプルフェッチコンテキスト(システム関数core.register_fetches(function)によって登録された関数のコンテキスト);
- コンバーターコンテキスト(システム関数core.register_converters(function)によって登録された関数のコンテキスト)。
実際には、ドキュメントに記載されていない別の実行コンテキストがあります。
- サービスコンテキスト(システム関数core.register_service(function)によって登録された関数のコンテキスト);
最も単純なHaproxyサーバー構成から始めましょう。構成は、フロントエンド(つまり、クライアントが要求を行うもの)とバックエンド(クライアントの要求がHaproxyサーバーを介してプロキシされる)の2つのセクションで構成されます。
frontend jwt mode http bind *:80 use_backend backend_app backend backend_app mode http server app1 app:3000
これで、Haproxyのポート80からのすべてのリクエストは、アプリサーバーのポート3000にリダイレクトされます。
サービス
Services — , Lua, . ore.register_service(function)).
Service guarde.lua:
function _M.hello_world(applet)
applet:set_status(200)
local response = string.format([[<html><body>Hello World!</body></html>]], message);
applet:add_header("content-type", "text/html");
applet:add_header("content-length", string.len(response))
applet:start_response()
applet:send(response)
end
Service register.lua:
package.path = package.path .. "./?.lua;/usr/local/etc/haproxy/?.lua"
local guard = require("guard")
core.register_service("hello-world", "http", guard.hello_world);
"http" , Service http (mode http).
Haproxy:
global lua-load /usr/local/etc/haproxy/register.lua frontend jwt mode http bind *:80 use_backend backend_app http-request use-service lua.hello-world if { path /hello_world } backend backend_app mode http server app1 app:3000
, Haproxy /hello_world, , lua.hello-world.
applet. .
Actions
Actions — , . Actions ( ) . Actions . Action. txn. Haproxy Action . Action, Bearer :
function _M.validate_token_action(txn)
local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ")
if auth_header[1] ~= "Bearer" or not auth_header[2] then
return txn:set_var("txn.not_authorized", true);
end
local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
if not claim then
return txn:set_var("txn.not_authorized", true);
end
if claim.exp < os.time() then
return txn:set_var("txn.authentication_timeout", true);
end
txn:set_var("txn.jwt_authorized", true);
end
Action:
core.register_action("validate-token", { "http-req" }, guard.validate_token_action);
{ "http-req" } , Action http ( ).
Haproxy, Action http-request:
frontend jwt mode http bind *:80 http-request use-service lua.hello-world if { path /hello_world } http-request lua.validate-token if { path -m beg /api/ }
, Action, ACL (Access Control Lists) — Haproxy:
acl jwt_authorized var(txn.jwt_authorized) -m bool use_backend app if jwt_authorized { path -m beg /api/ }
Haproxy Action validate-token:
global lua-load /usr/local/etc/haproxy/register.lua frontend jwt mode http bind *:80 http-request use-service lua.hello-world if { path /hello_world } http-request lua.validate-token if { path -m beg /api } acl bad_request var(txn.bad_request) -m bool acl not_authorized var(txn.not_authorized) -m bool acl authentication_timeout var(txn.authentication_timeout) -m bool acl too_many_request var(txn.too_many_request) -m bool acl jwt_authorized var(txn.jwt_authorized) -m bool http-request deny deny_status 400 if bad_request { path -m beg /api/ } http-request deny deny_status 401 if !jwt_authorized { path -m beg /api/ } || not_authorized { path -m beg /api/ } http-request return status 419 content-type text/html string "Authentication Timeout" if authentication_timeout { path -m beg /api/ } http-request deny deny_status 429 if too_many_request { path -m beg /api/ } http-request deny deny_status 429 if too_many_request { path -m beg /auth/ } use_backend app if { path /hello } use_backend app if { path /auth/login } use_backend app if jwt_authorized { path -m beg /api/ } backend app mode http server app1 app:3000
Fetches
Fetches — . , , Haproxy. , Fetch:
function _M.validate_token_fetch(txn)
local auth_header = core.tokenize(txn.sf:hdr("Authorization"), " ")
if auth_header[1] ~= "Bearer" or not auth_header[2] then
return "not_authorized";
end
local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
if not claim then
return "not_authorized";
end
if claim.exp < os.time() then
return "authentication_timeout";
end
return "jwt_authorized:" .. claim.jti;
end
core.register_fetches("validate-token", _M.validate_token_fetch);
ACL Fetches :
http-request set-var(txn.validate_token) lua.validate-token() acl bad_request var(txn.validate_token) == "bad_request" -m bool acl not_authorized var(txn.validate_token) == "not_authorized" -m bool acl authentication_timeout var(txn.validate_token) == "authentication_timeout" -m bool acl too_many_request var(txn.validate_token) == "too_many_request" -m bool acl jwt_authorized var(txn.validate_token) -m beg "jwt_authorized"
Converters
Converters . Converters, Fetches, , Haproxy. Haproxy Converters , , .
Converter, Authorization :
function _M.validate_token_converter(auth_header_string)
local auth_header = core.tokenize(auth_header_string, " ")
if auth_header[1] ~= "Bearer" or not auth_header[2] then
return "not_authorized";
end
local claim = jwt.decode(auth_header[2],{alg="RS256",keys={public=public_key}});
if not claim then
return "not_authorized";
end
if claim.exp < os.time() then
return "authentication_timeout";
end
return "jwt_authorized";
end
core.register_converters("validate-token-converter", _M.validate_token_converter);
:
http-request set-var(txn.validate_token) hdr(authorization),lua.validate-token-converter
Authorization, Fetch hdr() Converter lua.validate-token-converter.
Stick Table
Stick Table — -, , DDoS ( REST ). . Fetches Converters, , , cookie jti, . Stick Table . — ( ), , Haproxy. Stick Table:
stick-table type string size 100k expire 30s store http_req_rate(10s) http-request track-sc1 lua.validate-token() http-request deny deny_status 429 if { sc_http_req_rate(1) gt 3 }
1. . . 100k. 30 . 10 .
2. , Fetch lua.validate-token(), 1, (track-sc1)
3. , 2, 1 (sc_http_req_rate(1)) 3 — 429.
Actions
( ) — Actions . , . Haproxy c Nginx/Openresty Envoy, . Envoy , , . Openresty, , , Openresty. , Nodejs, — NIO ( -). - , Openresty Lua 5.1 Lua 5.2 5.3. Haproxy, Openresty, Lua . , Envoy, . , Openresty — , .
Redis. Stick Table. , . "" , "" . , . . , "" ( ) 100%. , . , . Redis, , :
function _M.validate_body(txn, keys, ttl, count, ip)
local body = txn.f:req_body();
local status, data = pcall(json.decode, body);
if not (status and type(data) == "table") then
return txn:set_var("txn.bad_request", true);
end
local redis_key = "validate:body"
for i, name in pairs(keys) do
if data[name] == nil or data[name] == "" then
return txn:set_var("txn.bad_request", true);
end
redis_key = redis_key .. ":" .. name .. ":" .. data[name]
end
if (ip) then
redis_key = redis_key .. ":ip:" .. ip
end
local test = _M.redis_incr(txn, redis_key, ttl, count);
end
function _M.redis_incr(txn, key, ttl, count)
local prefixed_key = "mobile:guard:" .. key
local tcp = core.tcp();
if tcp == nil then
return false;
end
tcp:settimeout(1);
if tcp:connect(redis_ip, redis_port) == nil then
return false;
end
local client = redis.connect({socket=tcp});
local status, result = pcall(client.set, client, prefixed_key, "0", "EX", ttl, "NX");
status, result = pcall(client.incrby, client, prefixed_key, 1);
tcp:close();
if tonumber(result) > count + 0.1 then
txn:set_var("txn.too_many_request", true)
return false;
else
return true;
end
end
core.register_action("validate-body", { "http-req" }, function(txn)
_M.validate_body(txn, {"name"}, 10, 2);
end);
この投稿で使用されているコードは、リポジトリで入手できます。特に、作業に必要な環境をセットアップするのに役立つdocker-compose.ymlファイルがあります。
apapacy@gmail.com2020年
12月5日