Added a whitelist capability

This commit is contained in:
Frans Veldman 2026-06-13 04:58:41 +00:00
commit f240382b16
4 changed files with 162 additions and 12 deletions

View file

@ -34,6 +34,140 @@ do
end
end
-- ---------- whitelist ----------
local WHITELIST_FILE = "/etc/nginx/captcha-whitelist.txt"
local wl4, wl6 = {}, {} -- {{net=..., mask=..., bits=...}, ...}
local function ip4_to_n(ip)
local a, b, c, d = ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$")
if not a then return nil end
a, b, c, d = tonumber(a), tonumber(b), tonumber(c), tonumber(d)
for _, n in ipairs({a, b, c, d}) do
if not n or n < 0 or n > 255 then return nil end
end
-- Build via arithmetic so this works on plain Lua 5.1 (no bit lib)
return ((a * 256 + b) * 256 + c) * 256 + d
end
-- Expand an IPv6 string to 8 groups of 16-bit numbers
local function ip6_to_groups(ip)
if not ip:find(":") then return nil end
local head, tail = ip:match("^(.-)::(.*)$")
local h_parts, t_parts = {}, {}
if head then
for g in (head .. ":"):gmatch("([^:]*):") do h_parts[#h_parts+1] = g end
for g in (tail .. ":"):gmatch("([^:]*):") do t_parts[#t_parts+1] = g end
if #h_parts == 1 and h_parts[1] == "" then h_parts = {} end
if #t_parts == 1 and t_parts[1] == "" then t_parts = {} end
else
for g in (ip .. ":"):gmatch("([^:]*):") do h_parts[#h_parts+1] = g end
end
local groups = {}
for _, g in ipairs(h_parts) do groups[#groups+1] = tonumber(g, 16) or -1 end
local fill = 8 - #h_parts - #t_parts
if head then
for _ = 1, fill do groups[#groups+1] = 0 end
end
for _, g in ipairs(t_parts) do groups[#groups+1] = tonumber(g, 16) or -1 end
if #groups ~= 8 then return nil end
for _, g in ipairs(groups) do
if g < 0 or g > 0xFFFF then return nil end
end
return groups
end
local function load_whitelist()
local new4, new6, count = {}, {}, 0
local f = io.open(WHITELIST_FILE, "r")
if not f then
ngx.log(ngx.ERR, "captcha: cannot open whitelist ", WHITELIST_FILE)
wl4, wl6 = {}, {}
return
end
for raw in f:lines() do
local line = raw:gsub("#.*$", ""):match("^%s*(.-)%s*$") or ""
if line ~= "" then
local addr, bits = line:match("^([^/]+)/(%d+)$")
if not addr then addr = line end
if addr:find(":", 1, true) then
local g = ip6_to_groups(addr)
if g then
bits = tonumber(bits or 128)
if bits >= 0 and bits <= 128 then
new6[#new6+1] = { groups = g, bits = bits }
count = count + 1
else
ngx.log(ngx.ERR, "captcha: bad whitelist line: ", raw)
end
else
ngx.log(ngx.ERR, "captcha: bad whitelist line: ", raw)
end
else
local n = ip4_to_n(addr)
if n then
bits = tonumber(bits or 32)
if bits >= 0 and bits <= 32 then
-- mask = 2^32 - 2^(32-bits)
local mask = (bits == 0) and 0
or (4294967296 - 2 ^ (32 - bits))
new4[#new4+1] = { net = n - (n % (2 ^ (32 - bits))),
mask = mask }
count = count + 1
else
ngx.log(ngx.ERR, "captcha: bad whitelist line: ", raw)
end
else
ngx.log(ngx.ERR, "captcha: bad whitelist line: ", raw)
end
end
end
end
f:close()
wl4, wl6 = new4, new6
ngx.log(ngx.ERR, "captcha: whitelist loaded, ", count, " entries")
end
local function ip_whitelisted(ip)
if not ip or ip == "" then return false end
if ip:find(":", 1, true) then
local g = ip6_to_groups(ip); if not g then return false end
for _, e in ipairs(wl6) do
local bits, ok = e.bits, true
for i = 1, 8 do
if bits >= 16 then
if g[i] ~= e.groups[i] then ok = false; break end
bits = bits - 16
elseif bits > 0 then
local shift = 16 - bits
local m = 0xFFFF - (2 ^ shift - 1)
if (g[i] - g[i] % (2 ^ shift)) ~= e.groups[i] - e.groups[i] % (2 ^ shift) then
ok = false
end
break
else
break
end
end
if ok then return true end
end
return false
else
local n = ip4_to_n(ip); if not n then return false end
for _, e in ipairs(wl4) do
-- n AND mask == net → network match
if (n - n % (4294967296 - e.mask + 0.5 - 0.5)) - (n % (2 ^ 0)) then
-- placeholder; see real check below
end
-- real check using arithmetic AND via subtraction of remainder:
local block = 4294967296 - e.mask
if (n - n % block) == e.net then return true end
end
return false
end
end
load_whitelist()
local function provider()
if cfg.cap_site_key ~= "" and cfg.cap_secret_key ~= "" and cfg.cap_url ~= "" then
@ -293,6 +427,7 @@ end
-- ---------- public: gate ----------
function M.guard()
if ngx.var.uri == VERIFY_PATH then return end
if ip_whitelisted(ngx.var.remote_addr) then return end
if cookie_is_valid(ngx.var["cookie_" .. COOKIE_NAME]) then return end
-- (re-add any monitoring bypasses you set up earlier here)