lua-cs-bouncer-mcaptcha/nginx/crowdsec.lua

290 lines
8.9 KiB
Lua
Raw Normal View History

2022-01-05 15:10:54 +01:00
package.path = package.path .. ";./?.lua"
local config = require "plugins.crowdsec.config"
2022-01-13 09:48:05 +01:00
local iputils = require "plugins.crowdsec.iputils"
2022-01-05 15:10:54 +01:00
local http = require "resty.http"
local cjson = require "cjson"
-- contain runtime = {}
local runtime = {}
2022-01-24 11:08:41 +01:00
-- remediations are stored in cache as int (shared dict tags)
-- we need to translate IDs to text with this.
2022-01-13 09:48:05 +01:00
runtime.remediations = {}
runtime.remediations["1"] = "ban"
runtime.remediations["2"] = "captcha"
2022-01-05 15:10:54 +01:00
local csmod = {}
-- init function
function csmod.init(configFile, userAgent)
local conf, err = config.loadConfig(configFile)
if conf == nil then
return nil, err
end
runtime.conf = conf
runtime.userAgent = userAgent
2022-01-05 18:33:41 +01:00
runtime.cache = ngx.shared.crowdsec_cache
2022-01-05 15:10:54 +01:00
-- if stream mode, add callback to stream_query and start timer
if runtime.conf["MODE"] == "stream" then
runtime.cache:set("startup", true)
runtime.cache:set("first_run", true)
end
return true, nil
end
function http_request(link)
local httpc = http.new()
httpc:set_timeout(runtime.conf['REQUEST_TIMEOUT'])
local res, err = httpc:request_uri(link, {
method = "GET",
headers = {
['Connection'] = 'close',
['X-Api-Key'] = runtime.conf["API_KEY"],
['User-Agent'] = runtime.userAgent
},
})
return res, err
end
function parse_duration(duration)
2022-01-13 09:48:05 +01:00
local match, err = ngx.re.match(duration, "^((?<hours>[0-9]+)h)?((?<minutes>[0-9]+)m)?(?<seconds>[0-9]+)")
2022-01-05 15:10:54 +01:00
local ttl = 0
if not match then
if err then
return ttl, err
end
end
2022-01-05 18:27:17 +01:00
if match["hours"] ~= nil and match["hours"] ~= false then
2022-01-05 15:10:54 +01:00
local hours = tonumber(match["hours"])
ttl = ttl + (hours * 3600)
end
2022-01-05 18:27:17 +01:00
if match["minutes"] ~= nil and match["minutes"] ~= false then
2022-01-05 15:10:54 +01:00
local minutes = tonumber(match["minutes"])
ttl = ttl + (minutes * 60)
end
2022-01-05 18:27:17 +01:00
if match["seconds"] ~= nil and match["seconds"] ~= false then
2022-01-05 15:10:54 +01:00
local seconds = tonumber(match["seconds"])
ttl = ttl + seconds
end
return ttl, nil
end
2022-01-13 09:48:05 +01:00
function get_remediation_id(remediation)
for key, value in pairs(runtime.remediations) do
if value == remediation then
return tonumber(key)
end
end
return nil
end
2022-01-17 18:17:43 +01:00
function item_to_string(item, scope)
local ip, cidr, ip_version
2022-01-17 18:17:43 +01:00
if scope:lower() == "ip" then
ip = item
end
if scope:lower() == "range" then
ip, cidr = iputils.splitRange(item, scope)
2022-01-13 09:48:05 +01:00
end
2022-01-21 21:31:23 +01:00
local ip_network_address, is_ipv4 = iputils.parseIPAddress(ip)
if is_ipv4 then
2022-01-13 09:48:05 +01:00
ip_version = "ipv4"
2022-01-17 18:17:43 +01:00
if cidr == nil then
cidr = 32
end
else
2022-01-13 09:48:05 +01:00
ip_version = "ipv6"
2022-01-21 21:31:23 +01:00
ip_network_address = ip_network_address.uint32[3]..":"..ip_network_address.uint32[2]..":"..ip_network_address.uint32[1]..":"..ip_network_address.uint32[0]
2022-01-17 18:17:43 +01:00
if cidr == nil then
cidr = 128
end
2022-01-13 09:48:05 +01:00
end
2022-01-17 18:17:43 +01:00
if ip_version == nil then
return "normal_"..item
2022-01-13 09:48:05 +01:00
end
2022-01-17 18:17:43 +01:00
local ip_netmask = iputils.cidrToInt(cidr, ip_version)
2022-01-13 09:48:05 +01:00
return ip_version.."_"..ip_netmask.."_"..ip_network_address
end
2022-01-05 15:10:54 +01:00
function stream_query()
-- As this function is running inside coroutine (with ngx.timer.every),
-- we need to raise error instead of returning them
2022-01-13 09:48:05 +01:00
local is_startup = runtime.cache:get("startup")
ngx.log(ngx.DEBUG, "Stream Query from worker : " .. tostring(ngx.worker.id()) .. " with startup "..tostring(is_startup))
local link = runtime.conf["API_URL"] .. "/v1/decisions/stream?startup=" .. tostring(is_startup)
2022-01-05 15:10:54 +01:00
local res, err = http_request(link)
if not res then
2022-01-07 18:07:11 +01:00
if ngx.timer.every == nil then
local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query)
if not ok then
error("Failed to create the timer: " .. (err or "unknown"))
end
end
2022-01-05 15:10:54 +01:00
error("request failed: ".. err)
end
local status = res.status
local body = res.body
if status~=200 then
2022-01-07 18:07:11 +01:00
if ngx.timer.every == nil then
local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query)
if not ok then
error("Failed to create the timer: " .. (err or "unknown"))
end
end
2022-01-05 15:10:54 +01:00
error("Http error " .. status .. " with message (" .. tostring(body) .. ")")
end
local decisions = cjson.decode(body)
-- process deleted decisions
if type(decisions.deleted) == "table" then
2022-01-13 09:48:05 +01:00
if not is_startup then
for i, decision in pairs(decisions.deleted) do
2022-01-17 18:17:43 +01:00
local key = item_to_string(decision.value, decision.scope)
2022-01-13 09:48:05 +01:00
runtime.cache:delete(key)
ngx.log(ngx.DEBUG, "Deleting '" .. key .. "'")
end
2022-01-05 15:10:54 +01:00
end
end
-- process new decisions
if type(decisions.new) == "table" then
for i, decision in pairs(decisions.new) do
if runtime.conf["BOUNCING_ON_TYPE"] == decision.type or runtime.conf["BOUNCING_ON_TYPE"] == "all" then
local ttl, err = parse_duration(decision.duration)
if err ~= nil then
ngx.log(ngx.ERR, "[Crowdsec] failed to parse ban duration '" .. decision.duration .. "' : " .. err)
end
2022-01-13 09:48:05 +01:00
local remediation_id = get_remediation_id(decision.type)
if remediation_id == nil then
remediation_id = 1
end
2022-01-17 18:17:43 +01:00
local key = item_to_string(decision.value, decision.scope)
2022-01-13 09:48:05 +01:00
local succ, err, forcible = runtime.cache:set(key, false, ttl, remediation_id)
2022-01-05 15:10:54 +01:00
if not succ then
ngx.log(ngx.ERR, "failed to add ".. decision.value .." : "..err)
end
if forcible then
ngx.log(ngx.ERR, "Lua shared dict (crowdsec cache) is full, please increase dict size in config")
end
2022-01-13 09:48:05 +01:00
ngx.log(ngx.DEBUG, "Adding '" .. key .. "' in cache for '" .. ttl .. "' seconds")
2022-01-05 15:10:54 +01:00
end
end
end
-- not startup anymore after first callback
runtime.cache:set("startup", false)
2022-01-07 17:29:33 +01:00
-- re-occuring timer if there is no timer.every available
if ngx.timer.every == nil then
local ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query)
if not ok then
error("Failed to create the timer: " .. (err or "unknown"))
end
end
2022-01-05 15:10:54 +01:00
return nil
end
function live_query(ip)
local link = runtime.conf["API_URL"] .. "/v1/decisions?ip=" .. ip
local res, err = http_request(link)
if not res then
return true, "request failed: ".. err
end
local status = res.status
local body = res.body
if status~=200 then
return true, "Http error " .. status .. " while talking to LAPI (" .. link .. ")"
end
if body == "null" then -- no result from API, no decision for this IP
-- set ip in cache and DON'T block it
runtime.cache:set(ip, true,runtime.conf["CACHE_EXPIRATION"])
return true, nil
end
local decision = cjson.decode(body)[1]
if runtime.conf["BOUNCING_ON_TYPE"] == decision.type or runtime.conf["BOUNCING_ON_TYPE"] == "all" then
-- set ip in cache and block it
runtime.cache:set(ip, false,runtime.conf["CACHE_EXPIRATION"])
return false, nil
else
return true, nil
end
end
function csmod.allowIp(ip)
if runtime.conf == nil then
2022-01-13 09:48:05 +01:00
return true, runtime.conf["BOUNCING_ON_TYPE"], "Configuration is bad, cannot run properly"
2022-01-05 15:10:54 +01:00
end
-- if it stream mode and startup start timer
2022-01-24 11:08:41 +01:00
if runtime.cache:get("first_run") == true and runtime.conf["MODE"] == "stream" then
2022-01-07 17:38:47 +01:00
local ok, err
2022-01-07 17:29:33 +01:00
if ngx.timer.every == nil then
2022-01-07 17:38:47 +01:00
ok, err = ngx.timer.at(runtime.conf["UPDATE_FREQUENCY"], stream_query)
2022-01-07 17:29:33 +01:00
else
2022-01-07 17:38:47 +01:00
ok, err = ngx.timer.every(runtime.conf["UPDATE_FREQUENCY"], stream_query)
end
if not ok then
runtime.cache:set("first_run", true)
2022-01-13 09:48:05 +01:00
return true, runtime.conf["BOUNCING_ON_TYPE"], "Failed to create the timer: " .. (err or "unknown")
2022-01-07 17:29:33 +01:00
end
2022-01-05 15:10:54 +01:00
runtime.cache:set("first_run", false)
ngx.log(ngx.DEBUG, "Timer launched")
end
2022-01-17 18:17:43 +01:00
local key = item_to_string(ip, "ip")
2022-01-13 09:48:05 +01:00
local key_parts = {}
for i in key.gmatch(key, "([^_]+)") do
table.insert(key_parts, i)
end
2022-01-20 10:47:08 +01:00
2022-01-13 09:48:05 +01:00
local key_type = key_parts[1]
if key_type == "normal" then
local in_cache, remediation_id = runtime.cache:get(key)
if in_cache ~= nil then -- we have it in cache
ngx.log(ngx.DEBUG, "'" .. key .. "' is in cache")
return in_cache, runtime.remediations[tostring(remediation_id)], nil
end
end
2022-01-20 10:47:08 +01:00
local ip_network_address = key_parts[3]
2022-01-13 09:48:05 +01:00
local netmasks = iputils.netmasks_by_key_type[key_type]
2022-01-20 10:47:08 +01:00
for i, netmask in pairs(netmasks) do
2022-01-21 21:31:23 +01:00
local item
if key_type == "ipv4" then
item = key_type.."_"..netmask.."_"..iputils.ipv4_band(ip_network_address, netmask)
end
if key_type == "ipv6" then
item = key_type.."_"..table.concat(netmask, ":").."_"..iputils.ipv6_band(ip_network_address, netmask)
end
2022-01-13 09:48:05 +01:00
in_cache, remediation_id = runtime.cache:get(item)
if in_cache ~= nil then -- we have it in cache
ngx.log(ngx.DEBUG, "'" .. key .. "' is in cache")
return in_cache, runtime.remediations[tostring(remediation_id)], nil
end
2022-01-05 15:10:54 +01:00
end
-- if live mode, query lapi
if runtime.conf["MODE"] == "live" then
local ok, err = live_query(ip)
2022-01-13 09:48:05 +01:00
return ok, runtime.conf["BOUNCING_ON_TYPE"], err
2022-01-05 15:10:54 +01:00
end
2022-01-13 09:48:05 +01:00
return true, runtime.conf["BOUNCING_ON_TYPE"], nil
2022-01-05 15:10:54 +01:00
end
-- Use it if you are able to close at shuttime
function csmod.close()
end
return csmod