- 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- 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-trafficinclude 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 (server name).
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
-- Get Authorization header (case-insensitive)
local headers = ngx.req.get_headers()
local auth = headers["Authorization"] or headers["authorization"]
local origin
-- Extract origin from the "Authorization: X-Matrix ..." header
-- Examples:
-- Authorization: X-Matrix origin="matrix.org",key="ed25519:abc",...
-- Authorization: X-Matrix key="ed25519:abc",origin=matrix.org,...
if auth then
local m, err = ngx.re.match(auth, [[\borigin="?([^",\s]+)"?]], "ijo")
if m then
origin = m[1]
end
end
-- Fallback to body (v1 invite PDUs may have "origin" in body)
if not origin then
ngx.req.read_body() -- explicitly read the req body
local data = ngx.req.get_body_data()
-- If body was buffered to disk (too large for memory buffer), read a small chunk.
if not data then
local body_file = ngx.req.get_body_file()
if body_file then
-- blocking file I/O. Keep it small to reduce impact.
local f = io.open(body_file, "rb")
if f then
data = f:read(8192) -- read only first 8KB to avoid heavy blocking
f:close()
end
end
end
if data then
local cjson = require "cjson.safe"
local obj = cjson.decode(data)
if obj then
-- Try common spots (legacy PDUs)
origin = obj.origin or (obj.event and obj.event.origin)
end
end
end
if not origin then
ngx.log(ngx.ERR, "federation_invite_no_origin_found")
return
end
-- normalize: strip a trailing :port and lowercase the hostname
do
local m = ngx.re.match(origin, [[^([^:]+)]], "jo")
origin = (m and m[1] or origin):lower()
end
local allow = { -- no rate limit
["safe.org"] = true,
["safe.net"] = true,
}
local deny = { -- reject immediately
["evil.org"] = true,
["evil.net"] = true,
}
if deny[origin] then
ngx.log(ngx.WARN, "federation_invite_access_denied_by_origin: ", origin)
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."}')
return ngx.exit(ngx.HTTP_FORBIDDEN)
end
if allow[origin] then
ngx.log(ngx.NOTICE, "federation_invite_access_allowed_by_origin: ", origin)
return
end
-- Rate limit by origin
ngx.log(ngx.NOTICE, "federation_invite_access_allowed_but_check_rate: ", origin)
-- 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)
local lim, err = limit_req.new("matrix_federation_invite", 2, 10)
if not lim then
ngx.log(ngx.ERR, "failed to instantiate resty.limit.req: ", err)
return ngx.exit(500)
end
-- origin server_name, must be unique in lua lua_shared_dict shm zone
-- delay, err = obj:incoming(key, commit)
local delay, err = lim:incoming(origin, true)
if not delay then
-- reject when over rate+burst
if err == "rejected" then
ngx.log(ngx.WARN, "federation_invite_rate_limit_request_rejected: ", origin)
return ngx.exit(503)
end
ngx.log(ngx.ERR, "failed to limit req: ", err)
return ngx.exit(500)
end
if delay >= 0.001 then
-- delay when between rate and rate+burst
ngx.log(ngx.WARN, "federation_invite_rate_limit_request_delayed: ", origin, " ", delay)
ngx.sleep(delay)
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.