Skip to content

Instantly share code, notes, and snippets.

@haproxytechblog
Last active July 31, 2023 21:50
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save haproxytechblog/55d25a9af8c3c363cc0cc5c9477d864d to your computer and use it in GitHub Desktop.
Save haproxytechblog/55d25a9af8c3c363cc0cc5c9477d864d to your computer and use it in GitHub Desktop.
5 Ways to Extend HAProxy with Lua
$ haproxy -vv | grep Lua
Built with Lua version : Lua 5.3.5
core.Debug("Hello HAProxy!\n")
global
lua-load /path/to/hello.lua
$ haproxy -d -f /etc/haproxy/haproxy.cfg
local function foo(txn, [param1], [param2], [etc.])
-- perform logic here
end
core.register_fetches("foo_fetch", foo)
txn.f:NAME_OF_FETCH()
core.register_fetches("greater_than", function(txn, var1, var2)
local number1 = tonumber(txn:get_var(var1))
local number2 = tonumber(txn:get_var(var2))
if number1 > number2 then return true
else return false end
end)
global
lua-load /path/to/greater_than.lua
frontend fe_main
bind :80
# Store the threshold in a variable
http-request set-var(txn.connrate_threshold) int(100)
stick-table type ip size 1m expire 10s store conn_rate(10s)
http-request track-sc0 src
# Store the connection rate in a variable
http-request set-var(txn.conn_rate) src_conn_rate
# Deny if rate is greater than threshold
http-request deny if { lua.greater_than(txn.conn_rate,txn.connrate_threshold) -m bool }
default_backend be_servers
if { var(txn.connrate_threshold),sub(txn.conn_rate) -m int lt 0 }
local function backend_with_least_sessions(txn)
-- Get the frontend that was used
local fe_name = txn.f:fe_name()
local least_sessions_backend = ""
local least_sessions = 99999999999
-- Loop through all the backends. You could change this
-- so that the backend names are passed into the function too.
for _, backend in pairs(core.backends) do
-- Look at only backends that have names that start with
-- the name of the frontend, e.g. "www_" prefix for "www" frontend.
if backend and backend.name:sub(1, #fe_name + 1) == fe_name .. '_' then
local total_sessions = 0
-- Using the backend, loop through each of its servers
for _, server in pairs(backend.servers) do
-- Get server's stats
local stats = server:get_stats()
-- Get the backend's total number of current sessions
if stats['status'] == 'UP' then
total_sessions = total_sessions + stats['scur']
core.Debug(backend.name .. ": " .. total_sessions)
end
end
if least_sessions > total_sessions then
least_sessions = total_sessions
least_sessions_backend = backend.name
end
end
end
-- Return the name of the backend that has the fewest sessions
core.Debug("Returning: " .. least_sessions_backend)
return least_sessions_backend
end
core.register_fetches('leastsess_backend', backend_with_least_sessions)
global
lua-load /path/to/least_sessions.lua
frontend www
bind :80
use_backend %[lua.leastsess_backend]
backend www_dc1
balance roundrobin
server server1 192.168.10.5:8080 check maxconn 30
backend www_dc2
balance roundrobin
server server1 192.168.11.5:8080 check maxconn 30
local function foo(value)
-- perform logic here
end
core.register_converters("foo_conv", foo)
local char_to_hex = function(c)
return string.format("%%%02X", string.byte(c))
end
local function urlencode(url)
if url == nil then
return
end
url = url:gsub("\n", "\r\n")
url = url:gsub("([^%w ])", char_to_hex)
url = url:gsub(" ", "+")
return url
end
core.register_converters("urlencode", urlencode)
global
lua-load /path/to/urlencode.lua
frontend fe_main
bind :80
# URL encode the company name and store it in variable.
# In practice, you could get a company ID from a cookie
# or URL parameter and then find the name via a map file.
http-request set-var(req.company) str("Vinyl & Rare Music"),lua.urlencode
# Redirect to new URL
http-request redirect prefix http://%[req.hdr(Host)]/%[var(req.company)] if { var(req.company) -m found } { path / }
default_backend be_servers
local function foo(txn)
-- perform logic here
end
core.register_action("foo_action", { 'tcp-req', 'tcp-res', 'http-req', 'http-res' }, foo, 0)
http-request lua.foo
$ sudo apt install -y python3 python3-flask
import random
from flask import Flask
app = Flask(__name__)
@app.route("/<address>")
def check(address=None):
myrandom = random.randint(0, 1)
if myrandom > 0:
return 'allow'
else:
return 'deny'
$ export FLASK_APP=ipchecker.py
$ flask run
* Serving Flask app "ipchecker"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
$ curl http://127.0.0.1:5000/192.168.50.1
local function check_ip(txn, addr, port)
if not addr then addr = '127.0.0.1' end
if not port then port = 5000 end
-- Set up a request to the service
local hdrs = {
[1] = string.format('host: %s:%s', addr, port),
[2] = 'accept: */*',
[3] = 'connection: close'
}
local req = {
[1] = string.format('GET /%s HTTP/1.1', tostring(txn.f:src())),
[2] = table.concat(hdrs, '\r\n'),
[3] = '\r\n'
}
req = table.concat(req, '\r\n')
-- Use core.tcp to get an instance of the Socket class
local socket = core.tcp()
socket:settimeout(5)
-- Connect to the service and send the request
if socket:connect(addr, port) then
if socket:send(req) then
-- Skip response headers
while true do
local line, _ = socket:receive('*l')
if not line then break end
if line == '' then break end
end
-- Get response body, if any
local content = socket:receive('*a')
-- Check if this request should be allowed
if content and content == 'allow' then
txn:set_var('req.blocked', false)
return
end
else
core.Alert('Could not connect to IP Checker server (send)')
end
socket:close()
else
core.Alert('Could not connect to IP Checker server (connect)')
end
-- The request should be blocked
txn:set_var('req.blocked', true)
end
core.register_action('checkip', {'http-req'}, check_ip, 2)
global
lua-load /path/to/ipchecker.lua
frontend fe_main
bind :80
http-request lua.checkip 127.0.0.1 5000
http-request deny if { var(req.blocked) -m bool }
default_backend be_servers
local function foo(applet)
-- perform logic here
end
core.register_service("foo_name", "[mode]", foo)
frontend fe_proxy
http-request use-service lua.foo_name
local function magic8ball(applet)
-- If client is POSTing request, receive body
-- local request = applet:receive()
local responses = {"Reply hazy", "Yes - definitely", "Don't count on it", "Outlook good", "Very doubtful"}
local myrandom = math.random(1, #responses)
local response = string.format([[
<html>
<body>%s</body>
</html>
]], responses[myrandom])
applet:set_status(200)
applet:add_header("content-length", string.len(response))
applet:add_header("content-type", "text/html")
applet:start_response()
applet:send(response)
end
core.register_service("magic8ball", "http", magic8ball)
global
lua-load /path/to/magic8ball.lua
frontend fe_main
bind :80
http-request use-service lua.magic8ball if { path /magic }
default_backend be_servers
fe_main fe_main/<lua.magic8ball> 0/0/0/0/0 200 125 - - ---- 1/1/0/0/0 0/0 "GET /magic HTTP/1.1"
local function foo()
-- perform logic here
end
core.register_task(foo)
local function log_work()
while true do
core.Debug("Doing some task work!\n")
core.msleep(10000)
end
end
core.register_task(log_work)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment