- https://www.lua.org/pil/contents.html lua book
- https://www.lua.org/manual/5.1/manual.html lua reference manual
- https://nginx-lua.readthedocs.io/en/latest/examples/lua-nginx-module/
- https://github.com/openresty/lua-nginx-module
- https://github.com/fabiocicerchia/nginx-lua/tree/main
- https://github.com/fabiocicerchia/nginx-lua/blob/main/docs/lua-nginx-module/nginx-api-for-lua.md
- https://github.com/spacewander/openresty-vim
- https://spec.matrix.org/v1.12/server-server-api/#put_matrixfederationv2inviteroomideventid
apt install libnginx-mod-http-lua
simple deny list based on the origin
field (server name) in the $request_body
:
server {
location ~ ^/_matrix/federation/(v1|v2)/invite/ {
access_by_lua_block {
ngx.req.read_body() -- explicitly read the req body
local data = ngx.req.get_body_data()
if data then
ngx.log(ngx.INFO, "federation_invite_request_body: ", data)
end
local match, err = ngx.re.match(data, '"origin"\\s*:\\s*"([^"]+)"', "jou")
local deny = {
["example.org"] = true,
["example.net"] = true,
}
if match then
if deny[match[1]] then
ngx.log(ngx.WARN, "federation_invite_access_denied_by_origin: ", match[1])
--ngx.sleep(60) -- wait a bit to waste spammers time?
ngx.status = ngx.HTTP_FORBIDDEN
ngx.header.content_type = 'application/json'
--ngx.header["Access-Control-Allow-Origin"] = '*'
--ngx.header["Access-Control-Allow-Methods"] = 'GET, HEAD, POST, PUT, DELETE, OPTIONS'
--ngx.header["Access-Control-Allow-Headers"] = 'X-Requested-With, Content-Type, Authorization, Date'
ngx.say('{"errcode":"M_FORBIDDEN","error":"User cannot invite the target user."}')
ngx.exit(ngx.HTTP_FORBIDDEN)
else
ngx.log(ngx.NOTICE, "federation_invite_access_allowed_by_origin: ", match[1])
end
else
ngx.log(ngx.ERR, "federation_invite_invalid_origin")
return
end
}
proxy_pass http://$workers;
proxy_read_timeout 600s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
client_max_body_size 125M;
}
}
- https://nginx-extras.getpagespeed.com/lua/limit-rate/
- https://github.com/openresty/lua-resty-limit-traffic
- https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/req.md
git clone https://github.com/openresty/lua-resty-limit-traffic.git /usr/local/lib/lua-resty-limit-traffic
include in nginx.conf
:
lua_package_path "/usr/local/lib/lua-resty-limit-traffic/lib/?.lua;;";
- https://github.com/openresty/lua-resty-limit-traffic#synopsis demonstrate the usage of the resty.limit.req module
allow/deny list and rate limit based on the origin
field (server name) in the $request_body
.
requests with server names in the deny
list will be hard rejected.
all others will be rate limited, except servers in the allow
list.
lua_shared_dict matrix_federation_invite 10m;
server {
location ~ ^/_matrix/federation/(v1|v2)/invite/ {
access_by_lua_block {
local method = ngx.req.get_method()
if method ~= 'PUT' then -- return if not PUT (lua '~=' means 'not equal')
return
end
ngx.req.read_body() -- explicitly read the req body
local data = ngx.req.get_body_data()
if data then
ngx.log(ngx.INFO, "federation_invite_request_body: ", data)
end
local match, err = ngx.re.match(data, '"origin"\\s*:\\s*"([^"]+)"', "jou")
local allow = { -- no rate limit
["safe.org"] = true,
["safe.net"] = true,
}
local deny = { -- reject immediately
["evil.org"] = true,
["evil.net"] = true,
}
if match then
if deny[match[1]] then
ngx.log(ngx.WARN, "federation_invite_access_denied_by_origin: ", match[1])
--ngx.sleep(60) -- wait a bit to waste spammers time?
ngx.status = ngx.HTTP_FORBIDDEN
ngx.header.content_type = 'application/json'
--ngx.header["Access-Control-Allow-Origin"] = '*'
--ngx.header["Access-Control-Allow-Methods"] = 'GET, HEAD, POST, PUT, DELETE, OPTIONS'
--ngx.header["Access-Control-Allow-Headers"] = 'X-Requested-With, Content-Type, Authorization, Date'
ngx.say('{"errcode":"M_FORBIDDEN","error":"User cannot invite the target user."}')
ngx.exit(ngx.HTTP_FORBIDDEN)
else
if allow[match[1]] then
ngx.log(ngx.NOTICE, "federation_invite_access_allowed_by_origin: ", match[1])
return
else
ngx.log(ngx.NOTICE, "federation_invite_access_allowed_but_check_rate: ", match[1])
-- see docs for more info:
-- https://github.com/openresty/lua-resty-limit-traffic/tree/master?tab=readme-ov-file#synopsis
-- https://github.com/openresty/lua-resty-limit-traffic/blob/master/lib/resty/limit/req.md#methods
local limit_req = require "resty.limit.req"
-- obj, err = class.new(shdict_name, rate, burst)
-- rate: requests/second, burst: requests (allow a 10 request burst and then limit 2 per second)
local lim, err = limit_req.new("matrix_federation_invite", 2, 10)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
return ngx.exit(500)
end
local key = match[1] -- origin server_name, must be unique in lua lua_shared_dict shm zone
-- delay, err = obj:incoming(key, commit)
local delay, err = lim:incoming(key, true)
if not delay then
-- reject when over rate+burst
if err == "rejected" then
ngx.log(ngx.WARN, "federation_invite_rate_limit_request_rejected: ", match[1])
return ngx.exit(503)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
if delay >= 0.001 then
local excess = err
-- delay when between rate and rate+burst
ngx.log(ngx.WARN, "federation_invite_rate_limit_request_delayed: ", match[1], " ", delay)
ngx.sleep(delay)
end
end
end
else
ngx.log(ngx.ERR, "federation_invite_invalid_origin")
return
end
}
proxy_pass http://$workers;
proxy_read_timeout 600s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
client_max_body_size 125M;
}
}
caveats:
this could be abused by someone to rate limit some other server. putting well-known trusted servers in the allow list and maybe using fail2ban should help to avoid this.