Haproxy - programação e configuração usando Lua

O servidor Haproxy possui recursos integrados para a execução de scripts Lua.







A linguagem de programação Lua é amplamente usada para estender as capacidades de vários servidores. Por exemplo, Lua pode ser programada para servidores Redis, Nginx (nginx-extras, openresty) e Envoy. Isso é bastante natural, uma vez que a linguagem de programação Lua foi desenvolvida apenas para facilitar a incorporação em aplicativos como uma linguagem de script.







Neste post, examinarei os casos de uso de Lua para estender os recursos do Haproxy.







De acordo com a documentação , os scripts Lua no servidor Haproxy podem ser executados em seis contextos:







  • contexto do corpo (contexto ao carregar a configuração do servidor Haproxy, quando os scripts especificados pela diretiva lua-load são executados);
  • contexto de inicialização (contexto de funções que são chamadas imediatamente após o carregamento da configuração e são registradas com a função do sistema core.register_init ( função );
  • contexto de tarefa (contexto de funções programadas registradas pela função do sistema core.register_task ( função ));
  • contexto de ação (contexto de funções registradas pela função do sistema core.register_action ( função ));
  • contexto de busca de amostra (contexto de funções registradas pela função do sistema core.register_fetches ( função ));
  • contexto do conversor (contexto das funções registradas pela função do sistema core.register_converters ( função )).


Na verdade, há outro contexto de execução que não está listado na documentação:







  • contexto de serviço (contexto de funções registradas pela função do sistema core.register_service ( função ));


Vamos começar com a configuração de servidor Haproxy mais simples. A configuração consiste em duas seções, o front-end - ou seja, para onde o cliente faz uma solicitação, e o back-end - onde a solicitação do cliente é enviada por proxy por meio do servidor Haproxy:







frontend jwt
        mode http
        bind *:80
        use_backend backend_app

backend backend_app
        mode http
        server app1 app:3000
      
      





Agora, todas as solicitações provenientes da porta 80 do Haproxy serão redirecionadas para a porta 3000 do servidor de aplicativos.







Serviços



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);
      
      





O código usado nesta postagem está disponível no repositório . Em particular, há um arquivo docker-compose.yml que o ajudará a configurar o ambiente de que você precisa para trabalhar.







apapacy@gmail.com

5 de dezembro de 2020








All Articles