Der Haproxy-Server verfügt über integrierte Funktionen zum Ausführen von Lua-Skripten.
Die Programmiersprache Lua wird häufig verwendet, um die Funktionen verschiedener Server zu erweitern. Zum Beispiel kann Lua für Redis, Nginx (Nginx-Extras, Openresty), Envoy-Server programmiert werden. Dies ist ganz natürlich, da die Programmiersprache Lua gerade entwickelt wurde, um das Einbetten in Anwendungen als Skriptsprache zu vereinfachen.
In diesem Beitrag gehe ich auf die Anwendungsfälle für Lua ein, um die Funktionen von Haproxy zu erweitern.
Laut Dokumentation können Lua-Skripte auf dem Haproxy-Server in sechs Kontexten ausgeführt werden:
- body context (Kontext beim Laden der Haproxy-Serverkonfiguration, wenn die in der lua-load-Direktive angegebenen Skripte ausgeführt werden);
- init context (Kontext von Funktionen, die unmittelbar nach dem Laden der Konfiguration aufgerufen und bei der Systemfunktion core.register_init ( function ) registriert werden ;
- Aufgabenkontext (Kontext der geplanten Funktionen, die von der Systemfunktion core.register_task ( function ) registriert wurden );
- Aktionskontext (Kontext der von der Systemfunktion core.register_action ( function ) registrierten Funktionen );
- Beispiel-Abruf-Kontext (Kontext von Funktionen, die von der Systemfunktion core.register_fetches ( Funktion ) registriert wurden );
- Konverterkontext (Kontext der von der Systemfunktion core.register_converters ( Funktion ) registrierten Funktionen ).
Es gibt tatsächlich einen anderen Ausführungskontext, der nicht in der Dokumentation aufgeführt ist:
- Dienstkontext (Kontext der von der Systemfunktion core.register_service ( Funktion ) registrierten Funktionen );
Beginnen wir mit der einfachsten Haproxy-Serverkonfiguration. Die Konfiguration besteht aus zwei Abschnitten: Frontend - dh, an was der Client eine Anfrage stellt, und Backend - wo die Anfrage des Clients über den Haproxy-Server weitergeleitet wird:
frontend jwt mode http bind *:80 use_backend backend_app backend backend_app mode http server app1 app:3000
Jetzt werden alle Anfragen, die an Port 80 von Haproxy eingehen, an Port 3000 des App-Servers umgeleitet.
Dienstleistungen
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);
Der in diesem Beitrag verwendete Code ist im Repository verfügbar . Insbesondere gibt es eine docker-compose.yml-Datei, mit der Sie die Umgebung einrichten können, die Sie zum Arbeiten benötigen.
apapacy@gmail.com
5. Dezember 2020