add new lib for nginx based only
This commit is contained in:
parent
b23acef8f9
commit
be02e32635
4 changed files with 300 additions and 0 deletions
8
nginx/access.lua
Normal file
8
nginx/access.lua
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
ok, err = require "crowdsec".allowIp(ngx.var.remote_addr)
|
||||||
|
if err ~= nil then
|
||||||
|
ngx.log(ngx.ERR, "[Crowdsec] bouncer error: " .. err)
|
||||||
|
end
|
||||||
|
if not ok then
|
||||||
|
ngx.log(ngx.ALERT, "[Crowdsec] denied '" .. ngx.var.remote_addr .. "'")
|
||||||
|
ngx.exit(ngx.HTTP_FORBIDDEN)
|
||||||
|
end
|
92
nginx/config.lua
Normal file
92
nginx/config.lua
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
local config = {}
|
||||||
|
|
||||||
|
function config.file_exists(file)
|
||||||
|
local f = io.open(file, "rb")
|
||||||
|
if f then
|
||||||
|
f:close()
|
||||||
|
end
|
||||||
|
return f ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function split(s, delimiter)
|
||||||
|
result = {};
|
||||||
|
for match in (s..delimiter):gmatch("(.-)"..delimiter.."(.-)") do
|
||||||
|
table.insert(result, match);
|
||||||
|
end
|
||||||
|
return result;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has_value (tab, val)
|
||||||
|
for index, value in ipairs(tab) do
|
||||||
|
if value == val then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function starts_with(str, start)
|
||||||
|
return str:sub(1, #start) == start
|
||||||
|
end
|
||||||
|
|
||||||
|
function config.loadConfig(file)
|
||||||
|
if not config.file_exists(file) then
|
||||||
|
return nil, "File ".. file .." doesn't exist"
|
||||||
|
end
|
||||||
|
local conf = {}
|
||||||
|
local valid_params = {'API_URL', 'API_KEY', 'BOUNCING_ON_TYPE', 'MODE'}
|
||||||
|
local valid_int_params = {'CACHE_EXPIRATION', 'CACHE_SIZE', 'REQUEST_TIMEOUT', 'UPDATE_FREQUENCY'}
|
||||||
|
local valid_bouncing_on_type_values = {'ban', 'captcha', 'all'}
|
||||||
|
local default_values = {
|
||||||
|
['REQUEST_TIMEOUT'] = 0.2,
|
||||||
|
['BOUNCING_ON_TYPE'] = "ban",
|
||||||
|
['MODE'] = "stream",
|
||||||
|
['UPDATE_FREQUENCY'] = 10
|
||||||
|
}
|
||||||
|
for line in io.lines(file) do
|
||||||
|
local isOk = false
|
||||||
|
if starts_with(line, "#") then
|
||||||
|
isOk = true
|
||||||
|
end
|
||||||
|
if not isOk then
|
||||||
|
local s = split(line, "=")
|
||||||
|
for k, v in pairs(s) do
|
||||||
|
if has_value(valid_params, v) then
|
||||||
|
if v == "BOUNCING_ON_TYPE" then
|
||||||
|
local value = s[2]
|
||||||
|
if not has_value(valid_bouncing_on_type_values, s[2]) then
|
||||||
|
ngx.log(ngx.ERR, "unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'ban' instead")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if v == "MODE" then
|
||||||
|
local value = s[2]
|
||||||
|
if not has_value({'stream', 'live'}, s[2]) then
|
||||||
|
ngx.log(ngx.ERR, "unsupported value '" .. s[2] .. "' for variable '" .. v .. "'. Using default value 'stream' instead")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local n = next(s, k)
|
||||||
|
conf[v] = s[n]
|
||||||
|
break
|
||||||
|
elseif has_value(valid_int_params, v) then
|
||||||
|
local n = next(s, k)
|
||||||
|
conf[v] = tonumber(s[n])
|
||||||
|
break
|
||||||
|
else
|
||||||
|
ngx.log(ngx.ERR, "unsupported configuration '" .. v .. "'")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for k, v in pairs(default_values) do
|
||||||
|
if conf[k] == nil then
|
||||||
|
conf[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return conf, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return config
|
195
nginx/crowdsec.lua
Normal file
195
nginx/crowdsec.lua
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
package.path = package.path .. ";./?.lua"
|
||||||
|
|
||||||
|
local config = require "plugins.crowdsec.config"
|
||||||
|
local http = require "resty.http"
|
||||||
|
local cjson = require "cjson"
|
||||||
|
cjson.decode_array_with_array_mt(true)
|
||||||
|
|
||||||
|
|
||||||
|
-- contain runtime = {}
|
||||||
|
local runtime = {}
|
||||||
|
|
||||||
|
|
||||||
|
function ipToInt( str )
|
||||||
|
local num = 0
|
||||||
|
if str and type(str)=="string" then
|
||||||
|
local o1,o2,o3,o4 = str:match("(%d+)%.(%d+)%.(%d+)%.(%d+)" )
|
||||||
|
num = 2^24*o1 + 2^16*o2 + 2^8*o3 + o4
|
||||||
|
end
|
||||||
|
return num
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
runtime.cache = ngx.shared.crowdsec
|
||||||
|
|
||||||
|
-- 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)
|
||||||
|
local match, err = ngx.re.match(duration, "((?<hours>[0-9]+)h)?((?<minutes>[0-9]+)m)?(?<seconds>[0-9]+)")
|
||||||
|
local ttl = 0
|
||||||
|
if not match then
|
||||||
|
if err then
|
||||||
|
return ttl, err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if match["hours"] ~= nil then
|
||||||
|
local hours = tonumber(match["hours"])
|
||||||
|
ttl = ttl + (hours * 3600)
|
||||||
|
end
|
||||||
|
if match["minutes"] ~= nil then
|
||||||
|
local minutes = tonumber(match["minutes"])
|
||||||
|
ttl = ttl + (minutes * 60)
|
||||||
|
end
|
||||||
|
if match["seconds"] ~= nil then
|
||||||
|
local seconds = tonumber(match["seconds"])
|
||||||
|
ttl = ttl + seconds
|
||||||
|
end
|
||||||
|
return ttl, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function stream_query()
|
||||||
|
-- As this function is running inside coroutine (with ngx.timer.every),
|
||||||
|
-- we need to raise error instead of returning them
|
||||||
|
ngx.log(ngx.DEBUG, "Stream Query from worker : " .. tostring(ngx.worker.id()) .. " with startup "..tostring(runtime.cache:get("startup")))
|
||||||
|
local link = runtime.conf["API_URL"] .. "/v1/decisions/stream?startup=" .. tostring(runtime.cache:get("startup"))
|
||||||
|
local res, err = http_request(link)
|
||||||
|
if not res then
|
||||||
|
error("request failed: ".. err)
|
||||||
|
end
|
||||||
|
|
||||||
|
local status = res.status
|
||||||
|
local body = res.body
|
||||||
|
if status~=200 then
|
||||||
|
error("Http error " .. status .. " with message (" .. tostring(body) .. ")")
|
||||||
|
end
|
||||||
|
|
||||||
|
local decisions = cjson.decode(body)
|
||||||
|
-- process deleted decisions
|
||||||
|
if type(decisions.deleted) == "table" then
|
||||||
|
for i, decision in pairs(decisions.deleted) do
|
||||||
|
runtime.cache:delete(decision.value)
|
||||||
|
ngx.log(ngx.DEBUG, "Deleting '" .. decision.value .. "'")
|
||||||
|
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
|
||||||
|
local succ, err, forcible = runtime.cache:set(decision.value, false, ttl)
|
||||||
|
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
|
||||||
|
ngx.log(ngx.DEBUG, "Adding '" .. decision.value .. "' in cache for '" .. ttl .. "' seconds")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- not startup anymore after first callback
|
||||||
|
runtime.cache:set("startup", false)
|
||||||
|
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
|
||||||
|
return true, "Configuration is bad, cannot run properly"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if it stream mode and startup start timer
|
||||||
|
if runtime.cache:get("first_run") == true then
|
||||||
|
local ok, err = ngx.timer.every(runtime.conf["UPDATE_FREQUENCY"], stream_query)
|
||||||
|
if not ok then
|
||||||
|
runtime.cache:set("first_run", true)
|
||||||
|
return true, "Failed to create the timer: " .. (err or "unknown")
|
||||||
|
end
|
||||||
|
runtime.cache:set("first_run", false)
|
||||||
|
ngx.log(ngx.DEBUG, "Timer launched")
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = runtime.cache:get(ip)
|
||||||
|
if data ~= nil then -- we have it in cache
|
||||||
|
ngx.log(ngx.DEBUG, "'" .. ip .. "' is in cache")
|
||||||
|
return data, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if live mode, query lapi
|
||||||
|
if runtime.conf["MODE"] == "live" then
|
||||||
|
local ok, err = live_query(ip)
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
return true, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Use it if you are able to close at shuttime
|
||||||
|
function csmod.close()
|
||||||
|
end
|
||||||
|
|
||||||
|
return csmod
|
5
nginx/recaptcha.lua
Normal file
5
nginx/recaptcha.lua
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
local recaptcha = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return recaptcha
|
Loading…
Reference in a new issue