diff --git a/TODO b/TODO index 28081bdce..7be137547 100644 --- a/TODO +++ b/TODO @@ -1,4 +1,5 @@ - load inline values for white/black/grey list core - bwcli with redis -- move bans to cachestore - direct access to ANTIBOT_URI without prepare_challenge call +- don't fail if session err is not nil (new session will be created) +- limit refactoring with redis scripts if needed diff --git a/src/bw/lua/bunkerweb/cachestore.lua b/src/bw/lua/bunkerweb/cachestore.lua index 537b79146..2d94bc57d 100644 --- a/src/bw/lua/bunkerweb/cachestore.lua +++ b/src/bw/lua/bunkerweb/cachestore.lua @@ -69,13 +69,12 @@ function cachestore:get(key) return {ret_get, ret_ttl} ]] local ret, err = clusterstore:call("eval", redis_script, 1, key) - -- local cjson = require "cjson" - -- require "bunkerweb.logger":new("DEBUG"):log(ngx.ERR, cjson.encode(ret)) if not ret then + clusterstore:close() return nil, err, nil end -- Extract values - clusterstore:close(redis) + clusterstore:close() if ret[1] == ngx.null then ret[1] = nil end diff --git a/src/bw/lua/bunkerweb/datastore.lua b/src/bw/lua/bunkerweb/datastore.lua index 8548d9a26..583c358b4 100644 --- a/src/bw/lua/bunkerweb/datastore.lua +++ b/src/bw/lua/bunkerweb/datastore.lua @@ -30,7 +30,7 @@ function datastore:keys() return self.dict:get_keys(0) end -function datastore:exp(key) +function datastore:ttl(key) local ttl, err = self.dict:ttl(key) if not ttl then return false, err diff --git a/src/bw/lua/bunkerweb/utils.lua b/src/bw/lua/bunkerweb/utils.lua index ce7c3ec1c..bca19536f 100644 --- a/src/bw/lua/bunkerweb/utils.lua +++ b/src/bw/lua/bunkerweb/utils.lua @@ -490,4 +490,96 @@ utils.get_session_var = function(key) return false, "no session" end +utils.is_banned = function(ip) + -- Check on local datastore + local reason, err = datastore:get("bans_ip_" .. ip) + if not reason and err ~= "not found" then + return nil, "datastore:get() error : " .. reason + elseif reason and err ~= "not found" then + local ok, ttl = datastore:ttl("bans_ip_" .. ip) + if not ok then + return true, reason, -1 + end + return true, reason, ttl + end + -- Redis case + local use_redis, err = utils.get_variable("USE_REDIS", false) + if not use_redis then + return nil, "can't get USE_REDIS variable : " .. err + elseif use_redis ~= "yes" then + return false, "not banned" + end + -- Connect + local clusterstore = require "bunkerweb.clusterstore":new() + local ok, err = clusterstore:connect() + if not ok then + return nil, "can't connect to redis server : " .. err + end + -- Redis atomic script : GET+TTL + local redis_script = [[ + local ret_get = redis.pcall("GET", KEYS[1]) + if type(ret_get) == "table" and ret_get["err"] ~= nil then + redis.log(redis.LOG_WARNING, "access GET error : " .. ret_get["err"]) + return ret_get + end + local ret_ttl = nil + if ret_get ~= nil then + ret_ttl = redis.pcall("TTL", KEYS[1]) + if type(ret_ttl) == "table" and ret_ttl["err"] ~= nil then + redis.log(redis.LOG_WARNING, "access TTL error : " .. ret_ttl["err"]) + return ret_ttl + end + end + return {ret_get, ret_ttl} + ]] + -- Execute redis script + local data, err = clusterstore:call("eval", redis_script, 1, "bans_ip_" .. ip) + if not data then + clusterstore:close() + return nil, "redis call error : " .. err + elseif data.err then + clusterstore:close() + return nil, "redis script error : " .. data.err + elseif data[1] ~= ngx.null then + clusterstore:close() + -- Update local cache + local ok, err = datastore:set("bans_ip_" .. ip, data[1], data[2]) + if not ok then + return nil, "datastore:set() error : " .. err + end + return true, data[1], data[2] + end + clusterstore:close() + return false, "not banned" +end + +utils.add_ban = function(ip, reason, ttl) + -- Set on local datastore + local ok, err = datastore:set("bans_ip_" .. ip, reason, ttl) + if not ok then + return false, "datastore:set() error : " .. err + end + -- Set on redis + local use_redis, err = utils.get_variable("USE_REDIS", false) + if not use_redis then + return nil, "can't get USE_REDIS variable : " .. err + elseif use_redis ~= "yes" then + return true, "success" + end + -- Connect + local clusterstore = require "bunkerweb.clusterstore":new() + local ok, err = clusterstore:connect() + if not ok then + return false, "can't connect to redis server : " .. err + end + -- SET call + local ok, err = clusterstore:call("set", "bans_ip_" .. ip, reason, "EX", ttl) + if not ok then + clusterstore:close() + return false, "redis SET failed : " .. err + end + clusterstore:close() + return true, "success" +end + return utils \ No newline at end of file diff --git a/src/common/confs/http.conf b/src/common/confs/http.conf index 2d756b175..5a3a681c5 100644 --- a/src/common/confs/http.conf +++ b/src/common/confs/http.conf @@ -53,6 +53,9 @@ lua_shared_dict cachestore_locks {{ CACHESTORE_LOCKS_MEMORY_SIZE }}; # LUA init block include /etc/nginx/init-lua.conf; +# LUA init worker block +include /etc/nginx/init-worker-lua.conf; + # API server {% if USE_API == "yes" %}include /etc/nginx/api.conf;{% endif +%} diff --git a/src/common/confs/init-worker-lua.conf b/src/common/confs/init-worker-lua.conf new file mode 100644 index 000000000..1a8bb3ea6 --- /dev/null +++ b/src/common/confs/init-worker-lua.conf @@ -0,0 +1,39 @@ +lua_shared_dict ready_lock 16k; + +init_worker_by_lua_block { + +-- Our timer function +local ready_log = function(premature) + -- Instantiate objects + local logger = require "bunkerweb.logger":new("INIT") + local datastore = require "bunkerweb.datastore":new() + local lock = require "resty.lock":new("ready_lock") + if not lock then + logger:log(ngx.ERR, "lock:new() failed : " .. err) + return + end + -- Acquire lock + local elapsed, err = lock:lock("ready") + if elapsed == nil then + logger:log(ngx.ERR, "lock:lock() failed : " .. err) + else + -- Display ready log + local ok, err = datastore:get("misc_ready") + if not ok and err ~= "not found" then + logger:log(ngx.ERR, "datastore:get() failed : " .. err) + elseif not ok and err == "not found" then + logger:log(ngx.NOTICE, "BunkerWeb is ready to fool hackers ! 🚀") + local ok, err = datastore:set("misc_ready", "ok") + if not ok then + logger:log(ngx.ERR, "datastore:set() failed : " .. err) + end + end + end + -- Release lock + lock:unlock() +end + +-- Start timer +ngx.timer.at(5, ready_log) + +} diff --git a/src/common/confs/server-http/access-lua.conf b/src/common/confs/server-http/access-lua.conf index ce3be5666..3e67dfc4a 100644 --- a/src/common/confs/server-http/access-lua.conf +++ b/src/common/confs/server-http/access-lua.conf @@ -5,6 +5,7 @@ local clogger = require "bunkerweb.logger" local helpers = require "bunkerweb.helpers" local utils = require "bunkerweb.utils" local cdatastore = require "bunkerweb.datastore" +local cclusterstore = require "bunkerweb.clusterstore" local cjson = require "cjson" -- Don't process internal requests @@ -31,12 +32,14 @@ end logger:log(ngx.INFO, "ngx.ctx filled (ret = " .. ret .. ")") -- Process bans as soon as possible -local reason, err = datastore:get("bans_ip_" .. ngx.ctx.bw.remote_addr) -if not reason and err ~= "not found" then - logger:log(ngx.ERR, "error while checking if client is banned : " .. reason) -elseif reason and err ~= "not found" then - logger:log(ngx.WARN, "IP " .. ngx.ctx.bw.remote_addr .. " is banned with reason : " .. reason) +local banned, reason, ttl = utils.is_banned(ngx.ctx.bw.remote_addr) +if banned == nil then + logger:log(ngx.ERR, "can't check if IP " .. ngx.ctx.bw.remote_addr .. " is banned : " .. reason) +elseif banned then + logger:log(ngx.WARN, "IP " .. ngx.ctx.bw.remote_addr .. " is banned with reason " .. reason .. " (" .. tostring(ttl) .. "s remaining)") return ngx.exit(utils.get_deny_status()) +else + logger:log(ngx.INFO, "IP " .. ngx.ctx.bw.remote_addr .. " is not banned") end -- Get plugins diff --git a/src/common/core/badbehavior/badbehavior.lua b/src/common/core/badbehavior/badbehavior.lua index c3b0119bd..cf022f2e7 100644 --- a/src/common/core/badbehavior/badbehavior.lua +++ b/src/common/core/badbehavior/badbehavior.lua @@ -53,7 +53,7 @@ function badbehavior.increase(premature, ip, count_time, ban_time, threshold, us local counter = false -- Redis case if use_redis then - local redis_counter, err = bad_behavior.redis_increase(ip, count_time, ban_time) + local redis_counter, err = badbehavior.redis_increase(ip, count_time, ban_time) if not redis_counter then logger:log(ngx.ERR, "(increase) redis_increase failed, falling back to local : " .. err) else @@ -84,9 +84,9 @@ function badbehavior.increase(premature, ip, count_time, ban_time, threshold, us end -- Store local ban if counter > threshold then - local ok, err = datastore:set("bans_ip_" .. ip, "bad behavior", ban_time) + local ok, err = utils.add_ban(ip, "bad behavior", ban_time) if not ok then - logger:log(ngx.ERR, "(increase) can't save ban to the datastore : " .. err) + logger:log(ngx.ERR, "(increase) can't save ban : " .. err) return end logger:log(ngx.WARN, "IP " .. ip .. " is banned for " .. ban_time .. "s (" .. tostring(counter) .. "/" .. tostring(threshold) .. ")") @@ -102,9 +102,9 @@ function badbehavior.decrease(premature, ip, count_time, threshold, use_redis) local counter = false -- Redis case if use_redis then - local redis_counter, err = badbehavior.redis_decrease(ip) + local redis_counter, err = badbehavior.redis_decrease(ip, count_time) if not redis_counter then - logger:log(ngx.ERR, "(increase) redis_increase failed, falling back to local : " .. err) + logger:log(ngx.ERR, "(decrease) redis_decrease failed, falling back to local : " .. err) else counter = redis_counter end @@ -113,7 +113,7 @@ function badbehavior.decrease(premature, ip, count_time, threshold, use_redis) if not counter then local local_counter, err = datastore:get("plugin_badbehavior_count_" .. ip) if not local_counter and err ~= "not found" then - logger:log(ngx.ERR, "(increase) can't get counts from the datastore : " .. err) + logger:log(ngx.ERR, "(decrease) can't get counts from the datastore : " .. err) end if local_counter == nil or local_counter <= 1 then counter = 0 @@ -123,11 +123,12 @@ function badbehavior.decrease(premature, ip, count_time, threshold, use_redis) end -- Store local counter if counter <= 0 then + counter = 0 local ok, err = datastore:delete("plugin_badbehavior_count_" .. ip) else local ok, err = datastore:set("plugin_badbehavior_count_" .. ip, counter, count_time) if not ok then - logger:log(ngx.ERR, "(increase) can't save counts to the datastore : " .. err) + logger:log(ngx.ERR, "(decrease) can't save counts to the datastore : " .. err) return end end @@ -149,7 +150,7 @@ function badbehavior.redis_increase(ip, count_time, ban_time) redis.log(redis.LOG_WARNING, "Bad behavior increase EXPIRE error : " .. ret_expire["err"]) return ret_expire end - if ret_incr > ARGV[2] then + if ret_incr > tonumber(ARGV[2]) then local ret_set = redis.pcall("SET", KEYS[2], "bad behavior", "EX", ARGV[2]) if type(ret_set) == "table" and ret_set["err"] ~= nil then redis.log(redis.LOG_WARNING, "Bad behavior increase SET error : " .. ret_set["err"]) diff --git a/src/common/core/redis/redis.lua b/src/common/core/redis/redis.lua.backup similarity index 96% rename from src/common/core/redis/redis.lua rename to src/common/core/redis/redis.lua.backup index 555a4e7d8..95c29f50b 100644 --- a/src/common/core/redis/redis.lua +++ b/src/common/core/redis/redis.lua.backup @@ -16,7 +16,7 @@ function redis:init() if self.variables["USE_REDIS"] ~= "yes" or self.is_loading then return self:ret(true, "init not needed") end - -- Check redis connection + -- Check redis connection () local ok, err = clusterstore:connect() if not ok then return self:ret(false, "redis connect error : " .. err) diff --git a/src/common/core/sessions/sessions.lua b/src/common/core/sessions/sessions.lua index 19abee9f9..8c9353fc9 100644 --- a/src/common/core/sessions/sessions.lua +++ b/src/common/core/sessions/sessions.lua @@ -19,16 +19,18 @@ function sessions:init() ["USE_REDIS"] = "", ["REDIS_HOST"] = "", ["REDIS_PORT"] = "", + ["REDIS_DATABASE"] = "", ["REDIS_SSL"] = "", ["REDIS_TIMEOUT"] = "", ["REDIS_KEEPALIVE_IDLE"] = "", ["REDIS_KEEPALIVE_POOL"] = "" } for k, v in pairs(redis_vars) do - local var, err = utils.get_variable(k, false) - if var == nil then + local value, err = utils.get_variable(k, false) + if value == nil then return self:ret(false, "can't get " .. k .. " variable : " .. err) end + redis_vars[k] = value end -- Init configuration local config = { @@ -58,7 +60,7 @@ function sessions:init() pool_size = tonumber(redis_vars["REDIS_KEEPALIVE_POOL"]), ssl = redis_vars["REDIS_SSL"] == "yes", host = redis_vars["REDIS_HOST"], - port = tonumber(redis_vars["REDIS_HOST"]), + port = tonumber(redis_vars["REDIS_PORT"]), database = tonumber(redis_vars["REDIS_DATABASE"]) } end