Haproxy - pemrograman dan konfigurasi menggunakan Lua

Server Haproxy memiliki fasilitas built-in untuk menjalankan skrip Lua.







Bahasa pemrograman Lua digunakan secara luas untuk memperluas kemampuan berbagai server. Misalnya, Lua dapat diprogram untuk server Redis, Nginx (nginx-extras, openresty), Envoy. Ini sangat wajar, karena bahasa pemrograman Lua baru saja dikembangkan untuk kemudahan penyematan ke dalam aplikasi sebagai bahasa scripting.







Dalam posting ini, saya akan membahas kasus penggunaan Lua untuk memperluas kemampuan Haproxy.







Menurut dokumentasi , skrip Lua di server Haproxy dapat berjalan dalam enam konteks:







  • konteks tubuh (konteks saat memuat konfigurasi server Haproxy, saat skrip yang ditentukan oleh direktif lua-load dijalankan);
  • konteks init (konteks fungsi yang dipanggil segera setelah memuat konfigurasi, dan terdaftar dengan fungsi sistem core.register_init ( function );
  • konteks tugas (konteks fungsi terjadwal yang didaftarkan oleh fungsi sistem core.register_task ( fungsi ));
  • konteks tindakan (konteks fungsi yang didaftarkan oleh fungsi sistem core.register_action ( function ));
  • konteks pengambilan sampel (konteks fungsi yang didaftarkan oleh fungsi sistem core.register_fetches ( fungsi ));
  • konteks konverter (konteks fungsi yang didaftarkan oleh fungsi sistem core.register_converters ( fungsi )).


Sebenarnya ada konteks eksekusi lain yang tidak tercantum dalam dokumentasi:







  • konteks layanan (konteks fungsi yang didaftarkan oleh fungsi sistem core.register_service ( function ));


Mari kita mulai dengan konfigurasi server Haproxy yang paling sederhana. Konfigurasi terdiri dari dua bagian, frontend - yaitu, apa yang diminta klien, dan backend - tempat permintaan klien diproksikan melalui server Haproxy:







frontend jwt
        mode http
        bind *:80
        use_backend backend_app

backend backend_app
        mode http
        server app1 app:3000
      
      





Sekarang semua permintaan yang masuk pada port 80 Haproxy akan dialihkan ke port 3000 dari server aplikasi.







Jasa



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





Kode yang digunakan dalam posting ini tersedia di repositori . Secara khusus, ada file docker-compose.yml yang akan membantu Anda mengatur lingkungan yang Anda butuhkan untuk bekerja.







apapacy@gmail.com

5 Desember 2020








All Articles