bunkerweb 1.4.0
This commit is contained in:
226
lua/api.lua
226
lua/api.lua
@@ -1,100 +1,85 @@
|
||||
local M = {}
|
||||
local api_list = {}
|
||||
local iputils = require "resty.iputils"
|
||||
local datastore = require "datastore"
|
||||
local utils = require "utils"
|
||||
local cjson = require "cjson"
|
||||
local plugins = require "plugins"
|
||||
local upload = require "resty.upload"
|
||||
local logger = require "logger"
|
||||
|
||||
api_list["^/ping$"] = function ()
|
||||
return true
|
||||
local api = {global = {GET = {}, POST = {}, PUT = {}, DELETE = {}}}
|
||||
|
||||
api.response = function(self, http_status, api_status, msg)
|
||||
local resp = {}
|
||||
resp["status"] = api_status
|
||||
resp["msg"] = msg
|
||||
return http_status, resp
|
||||
end
|
||||
|
||||
api_list["^/reload$"] = function ()
|
||||
local jobs = true
|
||||
local file = io.open("/etc/nginx/global.env", "r")
|
||||
for line in file:lines() do
|
||||
if line == "KUBERNETES_MODE=yes" or line == "SWARM_MODE=yes" then
|
||||
jobs = false
|
||||
break
|
||||
end
|
||||
api.global.GET["^/ping$"] = function(api)
|
||||
return api:response(ngx.HTTP_OK, "success", "pong")
|
||||
end
|
||||
|
||||
api.global.POST["^/jobs$"] = function(api)
|
||||
-- ngx.req.read_body()
|
||||
-- local data = ngx.req.get_body_data()
|
||||
-- if not data then
|
||||
-- local data_file = ngx.req.get_body_file()
|
||||
-- if data_file then
|
||||
-- local file = io.open(data_file)
|
||||
-- data = file:read("*a")
|
||||
-- file:close()
|
||||
-- end
|
||||
-- end
|
||||
-- local ok, env = pcall(cjson.decode, data)
|
||||
-- if not ok then
|
||||
-- return api:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. env)
|
||||
-- end
|
||||
-- local file = io.open("/opt/bunkerweb/tmp/jobs.env", "w+")
|
||||
-- for k, v in pairs(env) do
|
||||
-- file:write(k .. "=" .. v .. "\n")
|
||||
-- end
|
||||
-- file:close()
|
||||
local status = os.execute("/opt/bunkerweb/helpers/scheduler-restart.sh")
|
||||
if status == 0 then
|
||||
return api:response(ngx.HTTP_OK, "success", "jobs executed and scheduler started")
|
||||
end
|
||||
file:close()
|
||||
if jobs then
|
||||
os.execute("/opt/bunkerized-nginx/entrypoint/jobs.sh")
|
||||
return api:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status))
|
||||
end
|
||||
|
||||
api.global.POST["^/reload$"] = function(api)
|
||||
local status = os.execute("/usr/sbin/nginx -s reload")
|
||||
if status == 0 then
|
||||
return api:response(ngx.HTTP_OK, "success", "reload successful")
|
||||
end
|
||||
return os.execute("/usr/sbin/nginx -s reload") == 0
|
||||
return api:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status))
|
||||
end
|
||||
|
||||
api_list["^/stop$"] = function ()
|
||||
return os.execute("/usr/sbin/nginx -s quit") == 0
|
||||
end
|
||||
|
||||
api_list["^/stop%-temp$"] = function ()
|
||||
return os.execute("/usr/sbin/nginx -c /tmp/nginx-temp.conf -s stop") == 0
|
||||
end
|
||||
|
||||
api_list["^/conf$"] = function ()
|
||||
if not M.save_file("/tmp/conf.tar.gz") then
|
||||
return false
|
||||
api.global.POST["^/stop$"] = function(api)
|
||||
local status = os.execute("/usr/sbin/nginx -s quit")
|
||||
if status == 0 then
|
||||
return api:response(ngx.HTTP_OK, "success", "stop successful")
|
||||
end
|
||||
return M.extract_file("/tmp/conf.tar.gz", "/etc/nginx/")
|
||||
return api:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "exit status = " .. tostring(status))
|
||||
end
|
||||
|
||||
api_list["^/letsencrypt$"] = function ()
|
||||
if not M.save_file("/tmp/letsencrypt.tar.gz") then
|
||||
return false
|
||||
api.global.POST["^/confs$"] = function(api)
|
||||
local tmp = "/opt/bunkerweb/tmp/api_" .. ngx.var.uri:sub(2) .. ".tar.gz"
|
||||
local destination = "/opt/bunkerweb/" .. ngx.var.uri:sub(2)
|
||||
if ngx.var.uri == "/confs" then
|
||||
destination = "/etc/nginx"
|
||||
elseif ngx.var.uri == "/data" then
|
||||
destination = "/data"
|
||||
end
|
||||
return M.extract_file("/tmp/letsencrypt.tar.gz", "/etc/letsencrypt/")
|
||||
end
|
||||
|
||||
api_list["^/acme$"] = function ()
|
||||
if not M.save_file("/tmp/acme.tar.gz") then
|
||||
return false
|
||||
end
|
||||
return M.extract_file("/tmp/acme.tar.gz", "/acme-challenge")
|
||||
end
|
||||
|
||||
api_list["^/http$"] = function ()
|
||||
if not M.save_file("/tmp/http.tar.gz") then
|
||||
return false
|
||||
end
|
||||
return M.extract_file("/tmp/http.tar.gz", "/http-confs/")
|
||||
end
|
||||
|
||||
api_list["^/server$"] = function ()
|
||||
if not M.save_file("/tmp/server.tar.gz") then
|
||||
return false
|
||||
end
|
||||
return M.extract_file("/tmp/server.tar.gz", "/server-confs/")
|
||||
end
|
||||
|
||||
api_list["^/modsec$"] = function ()
|
||||
if not M.save_file("/tmp/modsec.tar.gz") then
|
||||
return false
|
||||
end
|
||||
return M.extract_file("/tmp/modsec.tar.gz", "/modsec-confs/")
|
||||
end
|
||||
|
||||
api_list["^/modsec%-crs$"] = function ()
|
||||
if not M.save_file("/tmp/modsec-crs.tar.gz") then
|
||||
return false
|
||||
end
|
||||
return M.extract_file("/tmp/modsec-crs.tar.gz", "/modsec-crs-confs/")
|
||||
end
|
||||
|
||||
function M.save_file (name)
|
||||
local form, err = upload:new(4096)
|
||||
if not form then
|
||||
logger.log(ngx.ERR, "API", err)
|
||||
return false
|
||||
return api:response(ngx.HTTP_BAD_REQUEST, "error", err)
|
||||
end
|
||||
form:set_timeout(1000)
|
||||
local file = io.open(name, "w")
|
||||
local file = io.open(tmp, "w+")
|
||||
while true do
|
||||
local typ, res, err = form:read()
|
||||
if not typ then
|
||||
file:close()
|
||||
logger.log(ngx.ERR, "API", "not typ")
|
||||
return false
|
||||
return api:response(ngx.HTTP_BAD_REQUEST, "error", err)
|
||||
end
|
||||
if typ == "eof" then
|
||||
break
|
||||
@@ -105,31 +90,86 @@ function M.save_file (name)
|
||||
end
|
||||
file:flush()
|
||||
file:close()
|
||||
return true
|
||||
local status = os.execute("rm -rf " .. destination .. "/*")
|
||||
if status ~= 0 then
|
||||
return api:response(ngx.HTTP_BAD_REQUEST, "error", "can't remove old files")
|
||||
end
|
||||
status = os.execute("tar xzf " .. tmp .. " -C " .. destination)
|
||||
if status ~= 0 then
|
||||
return api:response(ngx.HTTP_BAD_REQUEST, "error", "can't extract archive")
|
||||
end
|
||||
return api:response(ngx.HTTP_OK, "success", "saved data at " .. destination)
|
||||
end
|
||||
|
||||
function M.extract_file(archive, destination)
|
||||
return os.execute("tar xzf " .. archive .. " -C " .. destination) == 0
|
||||
api.global.POST["^/data$"] = api.global.POST["^/confs$"]
|
||||
|
||||
api.global.POST["^/unban$"] = function(api)
|
||||
ngx.req.read_body()
|
||||
local data = ngx.req.get_body_data()
|
||||
if not data then
|
||||
local data_file = ngx.req.get_body_file()
|
||||
if data_file then
|
||||
local file = io.open(data_file)
|
||||
data = file:read("*a")
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
local ok, ip = pcall(cjson.decode, data)
|
||||
if not ok then
|
||||
return api:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. env)
|
||||
end
|
||||
datastore:delete("bans_ip_" .. ip["ip"])
|
||||
return api:response(ngx.HTTP_OK, "success", "ip " .. ip["ip"] .. " unbanned")
|
||||
end
|
||||
|
||||
function M.is_api_call (api_uri, api_whitelist_ip)
|
||||
local whitelist = iputils.parse_cidrs(api_whitelist_ip)
|
||||
if iputils.ip_in_cidrs(ngx.var.remote_addr, whitelist) and ngx.var.request_uri:sub(1, #api_uri) .. "/" == api_uri .. "/" then
|
||||
for uri, code in pairs(api_list) do
|
||||
if string.match(ngx.var.request_uri:sub(#api_uri + 1), uri) then
|
||||
return true
|
||||
api.is_allowed_ip = function(self)
|
||||
local data, err = datastore:get("api_whitelist_ip")
|
||||
if not data then
|
||||
return false, "can't access api_allowed_ips in datastore"
|
||||
end
|
||||
if utils.is_ip_in_networks(ngx.var.remote_addr, cjson.decode(data).data) then
|
||||
return true, "ok"
|
||||
end
|
||||
return false, "IP is not in API_WHITELIST_IP"
|
||||
end
|
||||
|
||||
api.do_api_call = function(self)
|
||||
if self.global[ngx.var.request_method] ~= nil then
|
||||
for uri, api_fun in pairs(self.global[ngx.var.request_method]) do
|
||||
if string.match(ngx.var.uri, uri) then
|
||||
local status, resp = api_fun(self)
|
||||
local ret = true
|
||||
if status ~= ngx.HTTP_OK then
|
||||
ret = false
|
||||
end
|
||||
return ret, resp["msg"], status, cjson.encode(resp)
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function M.do_api_call (api_uri)
|
||||
for uri, code in pairs(api_list) do
|
||||
if string.match(ngx.var.request_uri:sub(#api_uri + 1), uri) then
|
||||
return code()
|
||||
local list, err = plugins:list()
|
||||
if not list then
|
||||
local status, resp = self:response(ngx.HTTP_INTERNAL_SERVER_ERROR, "error", "can't list loaded plugins : " .. err)
|
||||
return false, resp["msg"], ngx.HTTP_INTERNAL_SERVER_ERROR, resp
|
||||
end
|
||||
for i, plugin in ipairs(list) do
|
||||
if pcall(require, plugin.id .. "/" .. plugin.id) then
|
||||
local plugin_lua = require(plugin.id .. "/" .. plugin.id)
|
||||
if plugin_lua.api ~= nil then
|
||||
local matched, status, resp = plugin_lua.api()
|
||||
if matched then
|
||||
local ret = true
|
||||
if status ~= ngx.HTTP_OK then
|
||||
ret = false
|
||||
end
|
||||
return ret, resp["msg"], status, cjson.encode(resp)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local resp = {}
|
||||
resp["status"] = "error"
|
||||
resp["msg"] = "not found"
|
||||
return false, "error", ngx.HTTP_NOT_FOUND, cjson.encode(resp)
|
||||
end
|
||||
|
||||
return M
|
||||
return api
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
local M = {}
|
||||
local logger = require "logger"
|
||||
|
||||
function M.is_banned ()
|
||||
return ngx.shared.behavior_ban:get(ngx.var.remote_addr) == true
|
||||
end
|
||||
|
||||
function M.count (status_codes, threshold, count_time, ban_time)
|
||||
for k, v in ipairs(status_codes) do
|
||||
if v == tostring(ngx.status) then
|
||||
local count = ngx.shared.behavior_count:get(ngx.var.remote_addr)
|
||||
if count == nil then
|
||||
count = 0
|
||||
end
|
||||
count = count + 1
|
||||
local ok, err = ngx.shared.behavior_count:set(ngx.var.remote_addr, count, count_time)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "BEHAVIOR", "not enough memory allocated to behavior_ip_count")
|
||||
return false
|
||||
end
|
||||
if count >= threshold then
|
||||
logger.log(ngx.WARN, "BEHAVIOR", "threshold reached for " .. ngx.var.remote_addr .. " (" .. count .. " / " .. threshold .. ") : IP is banned for " .. ban_time .. " seconds")
|
||||
local ok, err = ngx.shared.behavior_ban:safe_set(ngx.var.remote_addr, true, ban_time)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "BEHAVIOR", "not enough memory allocated to behavior_ip_ban")
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,52 +0,0 @@
|
||||
local M = {}
|
||||
local dns = require "dns"
|
||||
local iputils = require "resty.iputils"
|
||||
local logger = require "logger"
|
||||
|
||||
function M.ip_cached_ko ()
|
||||
return ngx.shared.blacklist_ip_cache:get(ngx.var.remote_addr) == "ko"
|
||||
end
|
||||
|
||||
function M.reverse_cached_ko ()
|
||||
return ngx.shared.blacklist_reverse_cache:get(ngx.var.remote_addr) == "ko"
|
||||
end
|
||||
|
||||
function M.ip_cached ()
|
||||
return ngx.shared.blacklist_ip_cache:get(ngx.var.remote_addr) ~= nil
|
||||
end
|
||||
|
||||
function M.reverse_cached ()
|
||||
return ngx.shared.blacklist_reverse_cache:get(ngx.var.remote_addr) ~= nil
|
||||
end
|
||||
|
||||
function M.check_ip (ip_list)
|
||||
if #ip_list > 0 then
|
||||
local blacklist = iputils.parse_cidrs(ip_list)
|
||||
if iputils.ip_in_cidrs(ngx.var.remote_addr, blacklist) then
|
||||
ngx.shared.blacklist_ip_cache:set(ngx.var.remote_addr, "ko", 86400)
|
||||
logger.log(ngx.WARN, "BLACKLIST", "ip " .. ngx.var.remote_addr .. " is in blacklist")
|
||||
return true
|
||||
end
|
||||
end
|
||||
ngx.shared.blacklist_ip_cache:set(ngx.var.remote_addr, "ok", 86400)
|
||||
return false
|
||||
end
|
||||
|
||||
function M.check_reverse (reverse_list, resolvers)
|
||||
if #reverse_list > 0 then
|
||||
local rdns = dns.get_reverse(resolvers)
|
||||
if rdns ~= "" then
|
||||
for k, v in ipairs(reverse_list) do
|
||||
if rdns:sub(-#v) == v then
|
||||
ngx.shared.blacklist_reverse_cache:set(ngx.var.remote_addr, "ko", 86400)
|
||||
logger.log(ngx.WARN, "BLACKLIST", "reverse " .. rdns .. " is in blacklist")
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ngx.shared.blacklist_reverse_cache:set(ngx.var.remote_addr, "ok", 86400)
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,34 +0,0 @@
|
||||
local M = {}
|
||||
local captcha = require "misc.captcha"
|
||||
local base64 = require "misc.base64"
|
||||
|
||||
function M.get_challenge ()
|
||||
local cap = captcha.new()
|
||||
cap:font("/opt/bunkerized-nginx/lua/misc/Vera.ttf")
|
||||
cap:generate()
|
||||
return cap:jpegStr(70), cap:getStr()
|
||||
end
|
||||
|
||||
function M.get_code (img, antibot_uri)
|
||||
-- get template
|
||||
local f = io.open("/opt/bunkerized-nginx/antibot/captcha.html", "r")
|
||||
local template = f:read("*all")
|
||||
f:close()
|
||||
|
||||
-- get captcha code
|
||||
f = io.open("/opt/bunkerized-nginx/antibot/captcha.data", "r")
|
||||
local captcha_data = f:read("*all")
|
||||
f:close()
|
||||
|
||||
-- edit captcha code
|
||||
captcha_data = string.format(captcha_data, antibot_uri, base64.encode(img))
|
||||
|
||||
-- return template + edited captcha code
|
||||
return template:gsub("%%CAPTCHA%%", captcha_data)
|
||||
end
|
||||
|
||||
function M.check (captcha_user, captcha_valid)
|
||||
return captcha_user == captcha_valid
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,52 +0,0 @@
|
||||
local M = {}
|
||||
local redis = require "resty.redis"
|
||||
|
||||
local mt = { __index = M }
|
||||
|
||||
function M.new(self, name, data_dict, redis_client, type)
|
||||
return setmetatable({
|
||||
__name = name,
|
||||
__data_dict = data_dict,
|
||||
__redis_client = redis_client,
|
||||
__type = type
|
||||
}, mt)
|
||||
end
|
||||
|
||||
function M.check(self, data)
|
||||
-- without redis
|
||||
if self.__redis_client == nil then
|
||||
if self.__type == "simple" then
|
||||
local value, flags = self.__data_dict:get(data)
|
||||
return value ~= nil
|
||||
elseif self.__type == "match" then
|
||||
local patterns = self.__data_dict:get_keys(0)
|
||||
for i, pattern in ipairs(patterns) do
|
||||
if string.match(data, pattern) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- with redis
|
||||
else
|
||||
if self.__type == "simple" then
|
||||
local res, err = self.__redis_client:get(self.__name .. "_" .. data)
|
||||
return res and res ~= ngx.null
|
||||
elseif self.__type == "match" then
|
||||
local patterns = self.__redis_client:keys(self.__name .. "_*")
|
||||
if patterns then
|
||||
for i, pattern in ipairs(patterns) do
|
||||
local real_pattern = string.gsub(pattern, self.__name:gsub("%-", "%%-") .. "_", "", 1)
|
||||
if string.match(data, real_pattern) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,32 +0,0 @@
|
||||
local M = {}
|
||||
local session = require "resty.session"
|
||||
|
||||
function M.session ()
|
||||
if not ngx.ctx.session then
|
||||
ngx.ctx.session = session:start()
|
||||
end
|
||||
return ngx.ctx.session
|
||||
end
|
||||
|
||||
function M.is_set (key)
|
||||
local s = M.session()
|
||||
if s.data[key] then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function M.set (values)
|
||||
local s = M.session()
|
||||
for k, v in pairs(values) do
|
||||
s.data[k] = v
|
||||
end
|
||||
s:save()
|
||||
end
|
||||
|
||||
function M.get (key)
|
||||
local s = M.session ()
|
||||
return s.data[key]
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,70 +0,0 @@
|
||||
local M = {}
|
||||
local iputils = require "resty.iputils"
|
||||
local logger = require "logger"
|
||||
|
||||
function M.flush_dict (dict)
|
||||
local keys = dict:get_keys(0)
|
||||
for i, key in ipairs(keys) do
|
||||
dict:delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
function M.load_ip (path, dict)
|
||||
M.flush_dict(dict)
|
||||
local file = io.open(path, "r")
|
||||
if not file then
|
||||
logger.log(ngx.ERR, "INIT", "can't open " .. path)
|
||||
else
|
||||
io.input(file)
|
||||
local i = 0
|
||||
for line in io.lines() do
|
||||
local continue = true
|
||||
if string.match(line, "/") then
|
||||
local lower, upper = iputils.parse_cidr(line)
|
||||
local bin_ip = lower
|
||||
while bin_ip <= upper do
|
||||
local ok, err = dict:safe_set(bin_ip, true, 0)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "INIT", "not enough memory allocated to load data from " .. path)
|
||||
continue = false
|
||||
break
|
||||
end
|
||||
bin_ip = bin_ip + 1
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
local bin_ip, bin_octets = iputils.ip2bin(line)
|
||||
dict:set(bin_ip, true, 0)
|
||||
i = i + 1
|
||||
end
|
||||
if not continue then
|
||||
break
|
||||
end
|
||||
end
|
||||
logger.log(ngx.ERR, "INIT", "*NOT AN ERROR* loaded " .. tostring(i) .. " IPs from " .. path)
|
||||
io.close(file)
|
||||
end
|
||||
end
|
||||
|
||||
function M.load_raw (path, dict)
|
||||
M.flush_dict(dict)
|
||||
local file = io.open(path, "r")
|
||||
if not file then
|
||||
logger.log(ngx.ERR, "INIT", "can't open " .. path)
|
||||
else
|
||||
io.input(file)
|
||||
local i = 0
|
||||
for line in io.lines() do
|
||||
local ok, err = dict:safe_set(line, true, 0)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "INIT", "not enough memory allocated to load data from " .. path)
|
||||
break
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
logger.log(ngx.ERR, "INIT", "*NOT AN ERROR* loaded " .. tostring(i) .. " entries from " .. path)
|
||||
io.close(file)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
35
lua/datastore.lua
Normal file
35
lua/datastore.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
local datastore = {dict = ngx.shared.datastore }
|
||||
|
||||
datastore.get = function(self, key)
|
||||
local value, err = self.dict:get(key)
|
||||
if not value and not err then
|
||||
err = "not found"
|
||||
end
|
||||
return value, err
|
||||
end
|
||||
|
||||
datastore.set = function(self, key, value, exptime)
|
||||
exptime = exptime or 0
|
||||
return self.dict:safe_set(key, value, exptime)
|
||||
end
|
||||
|
||||
datastore.keys = function(self)
|
||||
return self.dict:get_keys(0)
|
||||
end
|
||||
|
||||
datastore.delete = function(self, key)
|
||||
self.dict:delete(key)
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
datastore.delete_all = function(self, pattern)
|
||||
local keys = self.dict:get_keys(0)
|
||||
for i, key in ipairs(keys) do
|
||||
if key:match(pattern) then
|
||||
self.dict:delete(key)
|
||||
end
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
return datastore
|
||||
43
lua/dns.lua
43
lua/dns.lua
@@ -1,43 +0,0 @@
|
||||
local M = {}
|
||||
local resolver = require "resty.dns.resolver"
|
||||
|
||||
function M.get_reverse(resolvers)
|
||||
local r, err = resolver:new{nameservers=resolvers, retrans=2, timeout=2000}
|
||||
if not r then
|
||||
return ""
|
||||
end
|
||||
local rdns = ""
|
||||
local answers, err = r:reverse_query(ngx.var.remote_addr)
|
||||
if answers ~= nil and not answers.errcode then
|
||||
for ak, av in ipairs(answers) do
|
||||
if av.ptrdname then
|
||||
rdns = av.ptrdname
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return rdns
|
||||
end
|
||||
|
||||
function M.get_ips(fqdn, resolvers)
|
||||
local r, err = resolver:new{nameservers=resolvers, retrans=2, timeout=2000}
|
||||
if not r then
|
||||
return ""
|
||||
end
|
||||
local ips = {}
|
||||
local answers, err, tries = r:query(fqdn, nil, {})
|
||||
if answers ~= nil then
|
||||
for ak, av in ipairs(answers) do
|
||||
if av.address then
|
||||
table.insert(ips, av.address)
|
||||
end
|
||||
end
|
||||
end
|
||||
return ips
|
||||
end
|
||||
|
||||
function M.ip_to_arpa()
|
||||
return resolver.arpa_str(ngx.var.remote_addr):gsub("%.in%-addr%.arpa", ""):gsub("%.ip6%.arpa", "")
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,37 +0,0 @@
|
||||
local M = {}
|
||||
local dns = require "dns"
|
||||
local logger = require "logger"
|
||||
local iputils = require "resty.iputils"
|
||||
|
||||
function M.cached_ko ()
|
||||
return ngx.shared.dnsbl_cache:get(ngx.var.remote_addr) == "ko"
|
||||
end
|
||||
|
||||
function M.cached ()
|
||||
return ngx.shared.dnsbl_cache:get(ngx.var.remote_addr) ~= nil
|
||||
end
|
||||
|
||||
function M.check (dnsbls, resolvers)
|
||||
local local_ips = iputils.parse_cidrs({"127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "10.0.0.0/8"})
|
||||
if iputils.ip_in_cidrs(ngx.var.remote_addr, local_ips) then
|
||||
ngx.shared.dnsbl_cache:set(ngx.var.remote_addr, "ok", 86400)
|
||||
return false
|
||||
end
|
||||
local rip = dns.ip_to_arpa()
|
||||
for k, v in ipairs(dnsbls) do
|
||||
local req = rip .. "." .. v
|
||||
local ips = dns.get_ips(req, resolvers)
|
||||
for k2, v2 in ipairs(ips) do
|
||||
local a,b,c,d = v2:match("([%d]+).([%d]+).([%d]+).([%d]+)")
|
||||
if a == "127" then
|
||||
ngx.shared.dnsbl_cache:set(ngx.var.remote_addr, "ko", 86400)
|
||||
logger.log(ngx.WARN, "DNSBL", "ip " .. ngx.var.remote_addr .. " is in DNSBL " .. v)
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
ngx.shared.dnsbl_cache:set(ngx.var.remote_addr, "ok", 86400)
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,43 +0,0 @@
|
||||
local M = {}
|
||||
local session = require "resty.session"
|
||||
|
||||
function M.get_challenge ()
|
||||
local charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIKLMNOPQRSTUVWXYZ0123456789"
|
||||
math.randomseed(os.clock()*os.time())
|
||||
local random = ""
|
||||
local rand = 0
|
||||
for i = 1, 20 do
|
||||
rand = math.random(1, #charset)
|
||||
random = random .. charset:sub(rand, rand)
|
||||
end
|
||||
return random
|
||||
end
|
||||
|
||||
function M.get_code (challenge, antibot_uri, original_uri)
|
||||
-- get template
|
||||
local f = io.open("/opt/bunkerized-nginx/antibot/javascript.html", "r")
|
||||
local template = f:read("*all")
|
||||
f:close()
|
||||
|
||||
-- get JS code
|
||||
f = io.open("/opt/bunkerized-nginx/antibot/javascript.data", "r")
|
||||
local javascript = f:read("*all")
|
||||
f:close()
|
||||
|
||||
-- edit JS code
|
||||
javascript = string.format(javascript, challenge, antibot_uri, original_uri)
|
||||
|
||||
-- return template + edited JS code
|
||||
return template:gsub("%%JAVASCRIPT%%", javascript)
|
||||
end
|
||||
|
||||
function M.check (challenge, user)
|
||||
local resty_sha256 = require "resty.sha256"
|
||||
local str = require "resty.string"
|
||||
local sha256 = resty_sha256:new()
|
||||
sha256:update(challenge .. user)
|
||||
local digest = sha256:final()
|
||||
return str.to_hex(digest):find("^0000") ~= nil
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,72 +0,0 @@
|
||||
local M = {}
|
||||
local logger = require "logger"
|
||||
|
||||
function M.decr (key, delay)
|
||||
local function callback (premature, key)
|
||||
if premature then
|
||||
ngx.shared.limit_req:delete(key)
|
||||
return
|
||||
end
|
||||
local value, flags = ngx.shared.limit_req:get(key)
|
||||
if value ~= nil then
|
||||
if value - 1 == 0 then
|
||||
ngx.shared.limit_req:delete(key)
|
||||
return
|
||||
end
|
||||
ngx.shared.limit_req:set(key, value-1, 0)
|
||||
end
|
||||
end
|
||||
local ok, err = ngx.timer.at(delay, callback, key)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "REQ LIMIT", "can't setup decrement timer : " .. err)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function M.incr (key)
|
||||
local newval, err, forcible = ngx.shared.limit_req:incr(key, 1, 0, 0)
|
||||
if not newval then
|
||||
logger.log(ngx.ERR, "REQ LIMIT", "can't increment counter : " .. err)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function M.check (rate, burst, sleep)
|
||||
local key = ngx.var.remote_addr .. ngx.var.uri
|
||||
local rate_split = {}
|
||||
for str in rate:gmatch("([^r/]+)") do
|
||||
table.insert(rate_split, str)
|
||||
end
|
||||
local max = tonumber(rate_split[1])
|
||||
local unit = rate_split[2]
|
||||
local delay = 0
|
||||
if unit == "s" then
|
||||
delay = 1
|
||||
elseif unit == "m" then
|
||||
delay = 60
|
||||
elseif unit == "h" then
|
||||
delay = 3600
|
||||
elseif unit == "d" then
|
||||
delay = 86400
|
||||
end
|
||||
if M.incr(key) then
|
||||
local current, flags = ngx.shared.limit_req:get(key)
|
||||
if M.decr(key, delay) then
|
||||
if current > max + burst then
|
||||
logger.log(ngx.WARN, "REQ LIMIT", "ip " .. ngx.var.remote_addr .. " has reached the limit for uri " .. ngx.var.uri .. " : " .. current .. "r/" .. unit .. " (max = " .. rate .. ")")
|
||||
return true
|
||||
elseif current > max then
|
||||
if sleep > 0 then
|
||||
ngx.sleep(sleep)
|
||||
end
|
||||
end
|
||||
else
|
||||
ngx.shared.limit_req:set(key, current-1, 0)
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,4 +1,4 @@
|
||||
local M = {}
|
||||
local M = {}
|
||||
local errlog = require "ngx.errlog"
|
||||
|
||||
function M.log (level, prefix, msg)
|
||||
@@ -6,3 +6,4 @@ function M.log (level, prefix, msg)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,202 +0,0 @@
|
||||
--[[
|
||||
|
||||
base64 -- v1.5.2 public domain Lua base64 encoder/decoder
|
||||
no warranty implied; use at your own risk
|
||||
|
||||
Needs bit32.extract function. If not present it's implemented using BitOp
|
||||
or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
|
||||
implementation inspired by Rici Lake's post:
|
||||
http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html
|
||||
|
||||
author: Ilya Kolbin (iskolbin@gmail.com)
|
||||
url: github.com/iskolbin/lbase64
|
||||
|
||||
COMPATIBILITY
|
||||
|
||||
Lua 5.1, 5.2, 5.3, LuaJIT
|
||||
|
||||
LICENSE
|
||||
|
||||
See end of file for license information.
|
||||
|
||||
--]]
|
||||
|
||||
|
||||
local base64 = {}
|
||||
|
||||
local extract = _G.bit32 and _G.bit32.extract
|
||||
if not extract then
|
||||
if _G.bit then
|
||||
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
|
||||
extract = function( v, from, width )
|
||||
return band( shr( v, from ), shl( 1, width ) - 1 )
|
||||
end
|
||||
elseif _G._VERSION >= "Lua 5.3" then
|
||||
extract = load[[return function( v, from, width )
|
||||
return ( v >> from ) & ((1 << width) - 1)
|
||||
end]]()
|
||||
else
|
||||
extract = function( v, from, width )
|
||||
local w = 0
|
||||
local flag = 2^from
|
||||
for i = 0, width-1 do
|
||||
local flag2 = flag + flag
|
||||
if v % flag2 >= flag then
|
||||
w = w + 2^i
|
||||
end
|
||||
flag = flag2
|
||||
end
|
||||
return w
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function base64.makeencoder( s62, s63, spad )
|
||||
local encoder = {}
|
||||
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
|
||||
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
|
||||
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
|
||||
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
|
||||
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
|
||||
encoder[b64code] = char:byte()
|
||||
end
|
||||
return encoder
|
||||
end
|
||||
|
||||
function base64.makedecoder( s62, s63, spad )
|
||||
local decoder = {}
|
||||
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
|
||||
decoder[charcode] = b64code
|
||||
end
|
||||
return decoder
|
||||
end
|
||||
|
||||
local DEFAULT_ENCODER = base64.makeencoder()
|
||||
local DEFAULT_DECODER = base64.makedecoder()
|
||||
|
||||
local char, concat = string.char, table.concat
|
||||
|
||||
function base64.encode( str, encoder, usecaching )
|
||||
encoder = encoder or DEFAULT_ENCODER
|
||||
local t, k, n = {}, 1, #str
|
||||
local lastn = n % 3
|
||||
local cache = {}
|
||||
for i = 1, n-lastn, 3 do
|
||||
local a, b, c = str:byte( i, i+2 )
|
||||
local v = a*0x10000 + b*0x100 + c
|
||||
local s
|
||||
if usecaching then
|
||||
s = cache[v]
|
||||
if not s then
|
||||
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
|
||||
cache[v] = s
|
||||
end
|
||||
else
|
||||
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
|
||||
end
|
||||
t[k] = s
|
||||
k = k + 1
|
||||
end
|
||||
if lastn == 2 then
|
||||
local a, b = str:byte( n-1, n )
|
||||
local v = a*0x10000 + b*0x100
|
||||
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
|
||||
elseif lastn == 1 then
|
||||
local v = str:byte( n )*0x10000
|
||||
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
|
||||
end
|
||||
return concat( t )
|
||||
end
|
||||
|
||||
function base64.decode( b64, decoder, usecaching )
|
||||
decoder = decoder or DEFAULT_DECODER
|
||||
local pattern = '[^%w%+%/%=]'
|
||||
if decoder then
|
||||
local s62, s63
|
||||
for charcode, b64code in pairs( decoder ) do
|
||||
if b64code == 62 then s62 = charcode
|
||||
elseif b64code == 63 then s63 = charcode
|
||||
end
|
||||
end
|
||||
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
|
||||
end
|
||||
b64 = b64:gsub( pattern, '' )
|
||||
local cache = usecaching and {}
|
||||
local t, k = {}, 1
|
||||
local n = #b64
|
||||
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
|
||||
for i = 1, padding > 0 and n-4 or n, 4 do
|
||||
local a, b, c, d = b64:byte( i, i+3 )
|
||||
local s
|
||||
if usecaching then
|
||||
local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
|
||||
s = cache[v0]
|
||||
if not s then
|
||||
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
|
||||
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
|
||||
cache[v0] = s
|
||||
end
|
||||
else
|
||||
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
|
||||
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
|
||||
end
|
||||
t[k] = s
|
||||
k = k + 1
|
||||
end
|
||||
if padding == 1 then
|
||||
local a, b, c = b64:byte( n-3, n-1 )
|
||||
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
|
||||
t[k] = char( extract(v,16,8), extract(v,8,8))
|
||||
elseif padding == 2 then
|
||||
local a, b = b64:byte( n-3, n-2 )
|
||||
local v = decoder[a]*0x40000 + decoder[b]*0x1000
|
||||
t[k] = char( extract(v,16,8))
|
||||
end
|
||||
return concat( t )
|
||||
end
|
||||
|
||||
return base64
|
||||
|
||||
--[[
|
||||
------------------------------------------------------------------------------
|
||||
This software is available under 2 licenses -- choose whichever you prefer.
|
||||
------------------------------------------------------------------------------
|
||||
ALTERNATIVE A - MIT License
|
||||
Copyright (c) 2018 Ilya Kolbin
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
------------------------------------------------------------------------------
|
||||
ALTERNATIVE B - Public Domain (www.unlicense.org)
|
||||
This is free and unencumbered software released into the public domain.
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
|
||||
software, either in source code form or as a compiled binary, for any purpose,
|
||||
commercial or non-commercial, and by any means.
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this
|
||||
software dedicate any and all copyright interest in the software to the public
|
||||
domain. We make this dedication for the benefit of the public at large and to
|
||||
the detriment of our heirs and successors. We intend this dedication to be an
|
||||
overt act of relinquishment in perpetuity of all present and future rights to
|
||||
this software under copyright law.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
------------------------------------------------------------------------------
|
||||
--]]
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
-- Copyright startx <startx@plentyfact.org>
|
||||
-- Modifications copyright mrDoctorWho <mrdoctorwho@gmail.com>
|
||||
-- Published under the MIT license
|
||||
|
||||
-- module("captcha", package.seeall)
|
||||
|
||||
local M = {}
|
||||
|
||||
local gd = require 'gd'
|
||||
|
||||
local mt = { __index = {} }
|
||||
|
||||
|
||||
function M.new()
|
||||
local cap = {}
|
||||
local f = setmetatable({ cap = cap}, mt)
|
||||
return f
|
||||
end
|
||||
|
||||
|
||||
local function urandom()
|
||||
local seed = 1
|
||||
local devurandom = io.open("/dev/urandom", "r")
|
||||
local urandom = devurandom:read(32)
|
||||
devurandom:close()
|
||||
|
||||
for i=1,string.len(urandom) do
|
||||
local s = string.byte(urandom,i)
|
||||
seed = seed + s
|
||||
end
|
||||
return seed
|
||||
end
|
||||
|
||||
|
||||
local function random_char(length)
|
||||
local set, char, uid
|
||||
local set = [[1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]]
|
||||
local captcha_t = {}
|
||||
|
||||
math.randomseed(urandom())
|
||||
|
||||
for c=1,length do
|
||||
local i = math.random(1, string.len(set))
|
||||
table.insert(captcha_t, string.sub(set,i,i))
|
||||
end
|
||||
|
||||
return captcha_t
|
||||
end
|
||||
|
||||
|
||||
local function random_angle()
|
||||
math.randomseed(urandom())
|
||||
return math.random(-20, 40)
|
||||
end
|
||||
|
||||
|
||||
local function scribble(w,h)
|
||||
math.randomseed(urandom())
|
||||
local x1 = math.random(5, w - 5)
|
||||
local x2 = math.random(5, w - 5)
|
||||
return x1, x2
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:string(s)
|
||||
self.cap.string = s
|
||||
end
|
||||
|
||||
function mt.__index:scribble(n)
|
||||
self.cap.scribble = n or 20
|
||||
end
|
||||
|
||||
function mt.__index:length(l)
|
||||
self.cap.length = l
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:bgcolor(r,g,b)
|
||||
self.cap.bgcolor = { r = r , g = g , b = b}
|
||||
end
|
||||
|
||||
function mt.__index:fgcolor(r,g,b)
|
||||
self.cap.fgcolor = { r = r , g = g , b = b}
|
||||
end
|
||||
|
||||
function mt.__index:line(line)
|
||||
self.cap.line = line
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:font(font)
|
||||
self.cap.font = font
|
||||
end
|
||||
|
||||
|
||||
function mt.__index:generate()
|
||||
--local self.captcha = {}
|
||||
local captcha_t = {}
|
||||
|
||||
if not self.cap.string then
|
||||
if not self.cap.length then
|
||||
self.cap.length = 6
|
||||
end
|
||||
captcha_t = random_char(self.cap.length)
|
||||
self:string(table.concat(captcha_t))
|
||||
else
|
||||
for i=1, #self.cap.string do
|
||||
table.insert(captcha_t, string.sub(self.cap.string, i, i))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
self.im = gd.createTrueColor(#captcha_t * 40, 45)
|
||||
local black = self.im:colorAllocate(0, 0, 0)
|
||||
local white = self.im:colorAllocate(255, 255, 255)
|
||||
local bgcolor
|
||||
if not self.cap.bgcolor then
|
||||
bgcolor = white
|
||||
else
|
||||
bgcolor = self.im:colorAllocate(self.cap.bgcolor.r , self.cap.bgcolor.g, self.cap.bgcolor.b )
|
||||
end
|
||||
|
||||
local fgcolor
|
||||
if not self.cap.fgcolor then
|
||||
fgcolor = black
|
||||
else
|
||||
fgcolor = self.im:colorAllocate(self.cap.fgcolor.r , self.cap.fgcolor.g, self.cap.fgcolor.b )
|
||||
end
|
||||
|
||||
self.im:filledRectangle(0, 0, #captcha_t * 40, 45, bgcolor)
|
||||
|
||||
local offset_left = 10
|
||||
|
||||
for i=1, #captcha_t do
|
||||
local angle = random_angle()
|
||||
local llx, lly, lrx, lry, urx, ury, ulx, uly = self.im:stringFT(fgcolor, self.cap.font, 25, math.rad(angle), offset_left, 35, captcha_t[i])
|
||||
self.im:polygon({ {llx, lly}, {lrx, lry}, {urx, ury}, {ulx, uly} }, bgcolor)
|
||||
offset_left = offset_left + 40
|
||||
end
|
||||
|
||||
if self.cap.line then
|
||||
self.im:line(10, 10, ( #captcha_t * 40 ) - 10 , 40, fgcolor)
|
||||
self.im:line(11, 11, ( #captcha_t * 40 ) - 11 , 41, fgcolor)
|
||||
self.im:line(12, 12, ( #captcha_t * 40 ) - 12 , 42, fgcolor)
|
||||
end
|
||||
|
||||
|
||||
if self.cap.scribble then
|
||||
for i=1,self.cap.scribble do
|
||||
local x1,x2 = scribble( #captcha_t * 40 , 45 )
|
||||
self.im:line(x1, 5, x2, 40, fgcolor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Perhaps it's not the best solution
|
||||
-- Writes the generated image to a jpeg file
|
||||
function mt.__index:jpeg(outfile, quality)
|
||||
self.im:jpeg(outfile, quality)
|
||||
end
|
||||
|
||||
-- Writes the generated image to a png file
|
||||
function mt.__index:png(outfile)
|
||||
self.im:png(outfile)
|
||||
end
|
||||
|
||||
-- Allows to get the image data in PNG format
|
||||
function mt.__index:pngStr()
|
||||
return self.im:pngStr()
|
||||
end
|
||||
|
||||
-- Allows to get the image data in JPEG format
|
||||
function mt.__index:jpegStr(quality)
|
||||
return self.im:jpegStr(quality)
|
||||
end
|
||||
|
||||
-- Allows to get the image text
|
||||
function mt.__index:getStr()
|
||||
return self.cap.string
|
||||
end
|
||||
|
||||
-- Writes the image to a file
|
||||
function mt.__index:write(outfile, quality)
|
||||
if self.cap.string == nil then
|
||||
self:generate()
|
||||
end
|
||||
self:jpeg(outfile, quality)
|
||||
-- Compatibility
|
||||
return self:getStr()
|
||||
end
|
||||
|
||||
return M
|
||||
3314
lua/misc/root-ca.pem
3314
lua/misc/root-ca.pem
File diff suppressed because it is too large
Load Diff
6
lua/mmdb.lua
Normal file
6
lua/mmdb.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local geoip = require "geoip.mmdb"
|
||||
|
||||
return {
|
||||
country_db = geoip.load_database("/opt/bunkerweb/cache/country.mmdb"),
|
||||
asn_db = geoip.load_database("/opt/bunkerweb/cache/asn.mmdb")
|
||||
}
|
||||
68
lua/plugins.lua
Normal file
68
lua/plugins.lua
Normal file
@@ -0,0 +1,68 @@
|
||||
local datastore = require "datastore"
|
||||
local cjson = require "cjson"
|
||||
|
||||
local plugins = {}
|
||||
|
||||
plugins.load = function(self, path)
|
||||
-- Read plugin.json file
|
||||
local file = io.open(path .. "/plugin.json")
|
||||
if not file then
|
||||
return false, "can't read plugin.json file"
|
||||
end
|
||||
|
||||
-- Decode plugin.json
|
||||
-- TODO : check return value of file:read and cjson.encode
|
||||
local data = cjson.decode(file:read("*a"))
|
||||
file:close()
|
||||
|
||||
-- Check required fields
|
||||
local required_fields = {"id", "order", "name", "description", "version", "settings"}
|
||||
for i, field in ipairs(required_fields) do
|
||||
if data[field] == nil then
|
||||
return false, "missing field " .. field .. " in plugin.json"
|
||||
end
|
||||
-- TODO : check values and types with regex
|
||||
end
|
||||
|
||||
-- Get existing plugins
|
||||
local list, err = plugins:list()
|
||||
if not list then
|
||||
return false, err
|
||||
end
|
||||
|
||||
-- Add our plugin to existing list and sort it
|
||||
table.insert(list, data)
|
||||
table.sort(list, function (a, b)
|
||||
return a.order < b.order
|
||||
end)
|
||||
|
||||
-- Save new plugin list in datastore
|
||||
local ok, err = datastore:set("plugins", cjson.encode(list))
|
||||
if not ok then
|
||||
return false, "can't save new plugin list"
|
||||
end
|
||||
|
||||
-- Save default settings value
|
||||
for variable, value in pairs(data.settings) do
|
||||
ok, err = datastore:set("plugin_" .. data.id .. "_" .. variable, value["default"])
|
||||
if not ok then
|
||||
return false, "can't save default variable value of " .. variable .. " into datastore"
|
||||
end
|
||||
end
|
||||
|
||||
-- Return the plugin
|
||||
return data, "success"
|
||||
end
|
||||
|
||||
plugins.list = function(self)
|
||||
-- Get encoded plugins from datastore
|
||||
local encoded_plugins, err = datastore:get("plugins")
|
||||
if not encoded_plugins then
|
||||
return false, "can't get encoded plugins from datastore"
|
||||
end
|
||||
|
||||
-- Decode and return the list
|
||||
return cjson.decode(encoded_plugins), "success"
|
||||
end
|
||||
|
||||
return plugins
|
||||
@@ -1,44 +0,0 @@
|
||||
local M = {}
|
||||
local http = require "resty.http"
|
||||
local cjson = require "cjson"
|
||||
|
||||
function M.get_code (antibot_uri, recaptcha_sitekey)
|
||||
-- get template
|
||||
local f = io.open("/opt/bunkerized-nginx/antibot/recaptcha.html", "r")
|
||||
local template = f:read("*all")
|
||||
f:close()
|
||||
|
||||
-- get recaptcha code
|
||||
f = io.open("/opt/bunkerized-nginx/antibot/recaptcha-head.data", "r")
|
||||
local recaptcha_head = f:read("*all")
|
||||
f:close()
|
||||
f = io.open("/opt/bunkerized-nginx/antibot/recaptcha-body.data", "r")
|
||||
local recaptcha_body = f:read("*all")
|
||||
f:close()
|
||||
|
||||
-- edit recaptcha code
|
||||
recaptcha_head = string.format(recaptcha_head, recaptcha_sitekey)
|
||||
recaptcha_body = string.format(recaptcha_body, antibot_uri, recaptcha_sitekey)
|
||||
|
||||
-- return template + edited recaptcha code
|
||||
return template:gsub("%%RECAPTCHA_HEAD%%", recaptcha_head):gsub("%%RECAPTCHA_BODY%%", recaptcha_body)
|
||||
end
|
||||
|
||||
function M.check (token, recaptcha_secret)
|
||||
local httpc = http.new()
|
||||
local res, err = httpc:request_uri("https://www.google.com/recaptcha/api/siteverify", {
|
||||
method = "POST",
|
||||
body = "secret=" .. recaptcha_secret .. "&response=" .. token .. "&remoteip=" .. ngx.var.remote_addr,
|
||||
headers = { ["Content-Type"] = "application/x-www-form-urlencoded" }
|
||||
})
|
||||
if not res then
|
||||
return 0.0
|
||||
end
|
||||
local data = cjson.decode(res.body)
|
||||
if not data.success then
|
||||
return 0.0
|
||||
end
|
||||
return data.score
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -1,104 +0,0 @@
|
||||
local M = {}
|
||||
local http = require "resty.http"
|
||||
local cjson = require "cjson"
|
||||
local logger = require "logger"
|
||||
|
||||
function M.send(method, url, data)
|
||||
local httpc, err = http.new()
|
||||
if not httpc then
|
||||
logger.log(ngx.ERR, "REMOTE API", "Can't instantiate HTTP object : " .. err)
|
||||
return false, nil, nil
|
||||
end
|
||||
local res, err = httpc:request_uri(ngx.shared.remote_api:get("server") .. url, {
|
||||
method = method,
|
||||
body = cjson.encode(data),
|
||||
headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
["User-Agent"] = "bunkerized-nginx/" .. data["version"]
|
||||
}
|
||||
})
|
||||
if not res then
|
||||
logger.log(ngx.ERR, "REMOTE API", "Can't send HTTP request : " .. err)
|
||||
return false, nil, nil
|
||||
end
|
||||
if res.status ~= 200 then
|
||||
logger.log(ngx.WARN, "REMOTE API", "Received status " .. res.status .. " from API : " .. res.body)
|
||||
end
|
||||
return true, res.status, cjson.decode(res.body)["data"]
|
||||
end
|
||||
|
||||
function M.gen_data(use_id, data)
|
||||
local all_data = {}
|
||||
if use_id then
|
||||
all_data["id"] = ngx.shared.remote_api:get("id")
|
||||
end
|
||||
all_data["version"] = ngx.shared.remote_api:get("version")
|
||||
for k, v in pairs(data) do
|
||||
all_data[k] = v
|
||||
end
|
||||
return all_data
|
||||
end
|
||||
|
||||
function M.ping2()
|
||||
local https = require "ssl.https"
|
||||
local ltn12 = require "ltn12"
|
||||
local request_body = cjson.encode(M.gen_data(true, {}))
|
||||
local response_body = {}
|
||||
local res, code, headers, status = https.request {
|
||||
url = ngx.shared.remote_api:get("server") .. "/ping",
|
||||
method = "GET",
|
||||
headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
["User-Agent"] = "bunkerized-nginx/" .. ngx.shared.remote_api:get("version"),
|
||||
["Content-Length"] = request_body:len()
|
||||
},
|
||||
source = ltn12.source.string(request_body),
|
||||
sink = ltn12.sink.table(response_body)
|
||||
}
|
||||
if res and status:match("^.*% 200% .*$") then
|
||||
response_body = cjson.decode(response_body[1])
|
||||
return response_body["data"] == "pong"
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function M.register()
|
||||
local request = {}
|
||||
local res, status, data = M.send("POST", "/register", M.gen_data(false, request))
|
||||
if res and status == 200 then
|
||||
return true, data
|
||||
end
|
||||
return false, data
|
||||
end
|
||||
|
||||
function M.ping()
|
||||
local request = {}
|
||||
local res, status, data = M.send("GET", "/ping", M.gen_data(true, request))
|
||||
if res and status == 200 then
|
||||
return true, data
|
||||
end
|
||||
return false, data
|
||||
end
|
||||
|
||||
function M.ip(ip, reason)
|
||||
local request = {
|
||||
["ip"] = ip,
|
||||
["reason"] = reason
|
||||
}
|
||||
local res, status, data = M.send("POST", "/ip", M.gen_data(true, request))
|
||||
if res and status == 200 then
|
||||
return true, data
|
||||
end
|
||||
return false, data
|
||||
end
|
||||
|
||||
function M.db()
|
||||
local request = {}
|
||||
local res, status, data = M.send("GET", "/db", M.gen_data(true, request))
|
||||
if res and status == 200 then
|
||||
return true, data
|
||||
end
|
||||
return false, data
|
||||
end
|
||||
|
||||
return M
|
||||
346
lua/utils.lua
Normal file
346
lua/utils.lua
Normal file
@@ -0,0 +1,346 @@
|
||||
local datastore = require "datastore"
|
||||
local ipmatcher = require "resty.ipmatcher"
|
||||
local cjson = require "cjson"
|
||||
local resolver = require "resty.dns.resolver"
|
||||
local mmdb = require "mmdb"
|
||||
local logger = require "logger"
|
||||
|
||||
local utils = {}
|
||||
|
||||
utils.set_values = function()
|
||||
local reserved_ips = {
|
||||
"0.0.0.0/8",
|
||||
"10.0.0.0/8",
|
||||
"100.64.0.0/10",
|
||||
"127.0.0.0/8",
|
||||
"169.254.0.0/16",
|
||||
"172.16.0.0/12",
|
||||
"192.0.0.0/24",
|
||||
"192.88.99.0/24",
|
||||
"192.168.0.0/16",
|
||||
"198.18.0.0/15",
|
||||
"198.51.100.0/24",
|
||||
"203.0.113.0/24",
|
||||
"224.0.0.0/4",
|
||||
"233.252.0.0/24",
|
||||
"240.0.0.0/4",
|
||||
"255.255.255.255/32"
|
||||
}
|
||||
local ok, err = datastore:set("misc_reserved_ips", cjson.encode({data = reserved_ips}))
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
local var_resolvers, err = datastore:get("variable_DNS_RESOLVERS")
|
||||
if not var_resolvers then
|
||||
return false, err
|
||||
end
|
||||
local list_resolvers = {}
|
||||
for str_resolver in var_resolvers:gmatch("%S+") do
|
||||
table.insert(list_resolvers, str_resolver)
|
||||
end
|
||||
ok, err = datastore:set("misc_resolvers", cjson.encode(list_resolvers))
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
return true, "success"
|
||||
end
|
||||
|
||||
utils.get_variable = function(var, site_search)
|
||||
if site_search == nil then
|
||||
site_search = true
|
||||
end
|
||||
local value, err = datastore:get("variable_" .. var)
|
||||
if not value then
|
||||
return nil, "Can't access variable " .. var .. " from datastore : " .. err
|
||||
end
|
||||
if site_search then
|
||||
local multisite, err = datastore:get("variable_MULTISITE")
|
||||
if not multisite then
|
||||
return nil, "Can't access variable MULTISITE from datastore : " .. err
|
||||
end
|
||||
if multisite == "yes" and ngx.var.server_name then
|
||||
local value_site, err = datastore:get("variable_" .. ngx.var.server_name .. "_" .. var)
|
||||
if value_site then
|
||||
value = value_site
|
||||
end
|
||||
end
|
||||
end
|
||||
return value, "success"
|
||||
end
|
||||
|
||||
utils.has_variable = function(var, value)
|
||||
local check_value, err = datastore:get("variable_" .. var)
|
||||
if not value then
|
||||
return nil, "Can't access variable " .. var .. " from datastore : " .. err
|
||||
end
|
||||
local multisite, err = datastore:get("variable_MULTISITE")
|
||||
if not multisite then
|
||||
return nil, "Can't access variable MULTISITE from datastore : " .. err
|
||||
end
|
||||
if multisite == "yes" then
|
||||
local servers, err = datastore:get("variable_SERVER_NAME")
|
||||
if not servers then
|
||||
return nil, "Can't access variable SERVER_NAME from datastore : " .. err
|
||||
end
|
||||
for server in servers:gmatch("%S+") do
|
||||
local check_value_site, err = datastore:get("variable_" .. server .. "_" .. var)
|
||||
if check_value_site and check_value_site == value then
|
||||
return true, "success"
|
||||
end
|
||||
end
|
||||
return false, "success"
|
||||
end
|
||||
return check_value == value, "success"
|
||||
end
|
||||
|
||||
utils.has_not_variable = function(var, value)
|
||||
local check_value, err = datastore:get("variable_" .. var)
|
||||
if not value then
|
||||
return nil, "Can't access variable " .. var .. " from datastore : " .. err
|
||||
end
|
||||
local multisite, err = datastore:get("variable_MULTISITE")
|
||||
if not multisite then
|
||||
return nil, "Can't access variable MULTISITE from datastore : " .. err
|
||||
end
|
||||
if multisite == "yes" then
|
||||
local servers, err = datastore:get("variable_SERVER_NAME")
|
||||
if not servers then
|
||||
return nil, "Can't access variable SERVER_NAME from datastore : " .. err
|
||||
end
|
||||
for server in servers:gmatch("%S+") do
|
||||
local check_value_site, err = datastore:get("variable_" .. server .. "_" .. var)
|
||||
if check_value_site and check_value_site ~= value then
|
||||
return true, "success"
|
||||
end
|
||||
end
|
||||
return false, "success"
|
||||
end
|
||||
return check_value ~= value, "success"
|
||||
end
|
||||
|
||||
function utils.get_multiple_variables(vars)
|
||||
local keys = datastore:keys()
|
||||
local result = {}
|
||||
for i, key in ipairs(keys) do
|
||||
for j, var in ipairs(vars) do
|
||||
local _, _, server, subvar = key:find("variable_(.*)_?(" .. var .. "_?%d*)")
|
||||
if subvar then
|
||||
if not server or server == "" then
|
||||
server = "global"
|
||||
else
|
||||
server = server:sub(1, -2)
|
||||
end
|
||||
if result[server] == nil then
|
||||
result[server] = {}
|
||||
end
|
||||
local value, err = datastore:get(key)
|
||||
if not value then
|
||||
return nil, err
|
||||
end
|
||||
result[server][subvar] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
utils.is_ip_in_networks = function(ip, networks)
|
||||
local ipm, err = ipmatcher.new(networks)
|
||||
if not ipm then
|
||||
return nil, "can't instantiate ipmatcher : " .. err
|
||||
end
|
||||
local matched, err = ipm:match(ip)
|
||||
if err then
|
||||
return nil, "can't check ip : " .. err
|
||||
end
|
||||
return matched
|
||||
end
|
||||
|
||||
utils.is_ipv4 = function(ip)
|
||||
return ipmatcher.parse_ipv4(ip)
|
||||
end
|
||||
|
||||
utils.is_ipv6 = function(ip)
|
||||
return ipmatcher.parse_ipv6(ip)
|
||||
end
|
||||
|
||||
utils.ip_is_global = function(ip)
|
||||
local data, err = datastore:get("misc_reserved_ips")
|
||||
if not data then
|
||||
return nil, "can't get reserved ips : " .. err
|
||||
end
|
||||
local ok, reserved_ips = pcall(cjson.decode, data)
|
||||
if not ok then
|
||||
return nil, "can't decode json : " .. reserved_ips
|
||||
end
|
||||
local ipm, err = ipmatcher.new(reserved_ips.data)
|
||||
if not ipm then
|
||||
return nil, "can't instantiate ipmatcher : " .. err
|
||||
end
|
||||
local matched, err = ipm:match(ip)
|
||||
if err then
|
||||
return nil, "can't check ip : " .. err
|
||||
end
|
||||
return not matched, "success"
|
||||
end
|
||||
|
||||
utils.get_integration = function()
|
||||
local integration, err = datastore:get("misc_integration")
|
||||
if integration then
|
||||
return integration
|
||||
end
|
||||
local var, err = datastore:get("variable_SWARM_MODE")
|
||||
if var == "yes" then
|
||||
integration = "swarm"
|
||||
else
|
||||
local var, err = datastore:get("variable_KUBERNETES_MODE")
|
||||
if var == "yes" then
|
||||
integration = "kubernetes"
|
||||
else
|
||||
local f, err = io.open("/etc/os-release", "r")
|
||||
if f then
|
||||
local data = f:read("*a")
|
||||
if data:find("Alpine") then
|
||||
integration = "docker"
|
||||
else
|
||||
integration = "unknown"
|
||||
end
|
||||
f:close()
|
||||
else
|
||||
integration = "unknown"
|
||||
end
|
||||
end
|
||||
end
|
||||
local ok, err = datastore:set("misc_integration", integration)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "UTILS", "Can't cache integration to datastore : " .. err)
|
||||
end
|
||||
return integration
|
||||
end
|
||||
|
||||
utils.get_version = function()
|
||||
local version, err = datastore:get("misc_version")
|
||||
if version then
|
||||
return version
|
||||
end
|
||||
local f, err = io.open("/opt/bunkerweb/VERSION", "r")
|
||||
if not f then
|
||||
logger.log(ngx.ERR, "UTILS", "Can't read VERSION file : " .. err)
|
||||
return "unknown"
|
||||
end
|
||||
version = f:read("*a")
|
||||
f:close()
|
||||
local ok, err = datastore:set("misc_version", version)
|
||||
if not ok then
|
||||
logger.log(ngx.ERR, "UTILS", "Can't cache version to datastore : " .. err)
|
||||
end
|
||||
return version
|
||||
end
|
||||
|
||||
utils.get_reason = function()
|
||||
if ngx.var.reason and ngx.var.reason ~= "" then
|
||||
return ngx.var.reason
|
||||
end
|
||||
if os.getenv("REASON") == "modsecurity" then
|
||||
return "modsecurity"
|
||||
end
|
||||
if ngx.status == ngx.HTTP_FORBIDDEN then
|
||||
return "unknown"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
utils.get_rdns = function(ip)
|
||||
local str_resolvers, err = datastore:get("misc_resolvers")
|
||||
if not str_resolvers then
|
||||
return false, err
|
||||
end
|
||||
local resolvers = cjson.decode(str_resolvers)
|
||||
local rdns, err = resolver:new{
|
||||
nameservers = resolvers,
|
||||
retrans = 1,
|
||||
timeout = 1000
|
||||
}
|
||||
if not rdns then
|
||||
return false, err
|
||||
end
|
||||
local answers, err = rdns:reverse_query(ip)
|
||||
if not answers then
|
||||
return false, err
|
||||
end
|
||||
if answers.errcode then
|
||||
return false, answers.errstr
|
||||
end
|
||||
for i, answer in ipairs(answers) do
|
||||
if answer.ptrdname then
|
||||
return answer.ptrdname, "success"
|
||||
end
|
||||
end
|
||||
return false, nil
|
||||
end
|
||||
|
||||
utils.get_ips = function(fqdn, resolvers)
|
||||
local str_resolvers, err = datastore:get("misc_resolvers")
|
||||
if not str_resolvers then
|
||||
return false, err
|
||||
end
|
||||
local resolvers = cjson.decode(str_resolvers)
|
||||
local rdns, err = resolver:new{
|
||||
nameservers = resolvers,
|
||||
retrans = 1,
|
||||
timeout = 1000
|
||||
}
|
||||
if not rdns then
|
||||
return false, err
|
||||
end
|
||||
local answers, err = rdns:query(fqdn, nil, {})
|
||||
if not answers then
|
||||
return false, err
|
||||
end
|
||||
if answers.errcode then
|
||||
return {}, answers.errstr
|
||||
end
|
||||
local ips = {}
|
||||
for i, answer in ipairs(answers) do
|
||||
if answer.address then
|
||||
table.insert(ips, answer.addres)
|
||||
end
|
||||
end
|
||||
return ips, "success"
|
||||
end
|
||||
|
||||
utils.get_country = function(ip)
|
||||
if not mmdb.country_db then
|
||||
return false, "mmdb country not loaded"
|
||||
end
|
||||
local result, err = mmdb.country_db:lookup(ip)
|
||||
if not result then
|
||||
return nil, err
|
||||
end
|
||||
return result.country.iso_code, "success"
|
||||
end
|
||||
|
||||
utils.get_asn = function(ip)
|
||||
if not mmdb.asn_db then
|
||||
return false, "mmdb asn not loaded"
|
||||
end
|
||||
local result, err = mmdb.asn_db:lookup(ip)
|
||||
if not result then
|
||||
return nil, err
|
||||
end
|
||||
return result.autonomous_system_number, "success"
|
||||
end
|
||||
|
||||
utils.rand = function(nb)
|
||||
local charset = {}
|
||||
for i = 48, 57 do table.insert(charset, string.char(i)) end
|
||||
for i = 65, 90 do table.insert(charset, string.char(i)) end
|
||||
for i = 97, 122 do table.insert(charset, string.char(i)) end
|
||||
local result = ""
|
||||
for i = 1, nb do
|
||||
result = result .. charset[math.random(1, #charset)]
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return utils
|
||||
@@ -1,62 +0,0 @@
|
||||
local M = {}
|
||||
local dns = require "dns"
|
||||
local iputils = require "resty.iputils"
|
||||
local logger = require "logger"
|
||||
|
||||
function M.ip_cached_ok ()
|
||||
return ngx.shared.whitelist_ip_cache:get(ngx.var.remote_addr) == "ok"
|
||||
end
|
||||
|
||||
function M.reverse_cached_ok ()
|
||||
return ngx.shared.whitelist_reverse_cache:get(ngx.var.remote_addr) == "ok"
|
||||
end
|
||||
|
||||
function M.ip_cached ()
|
||||
return ngx.shared.whitelist_ip_cache:get(ngx.var.remote_addr) ~= nil
|
||||
end
|
||||
|
||||
function M.reverse_cached ()
|
||||
return ngx.shared.whitelist_reverse_cache:get(ngx.var.remote_addr) ~= nil
|
||||
end
|
||||
|
||||
function M.check_ip (ip_list)
|
||||
if #ip_list > 0 then
|
||||
local whitelist = iputils.parse_cidrs(ip_list)
|
||||
if iputils.ip_in_cidrs(ngx.var.remote_addr, whitelist) then
|
||||
ngx.shared.whitelist_ip_cache:set(ngx.var.remote_addr, "ok", 86400)
|
||||
logger.log(ngx.NOTICE, "WHITELIST", "ip " .. ngx.var.remote_addr .. " is in whitelist")
|
||||
return true
|
||||
end
|
||||
end
|
||||
ngx.shared.whitelist_ip_cache:set(ngx.var.remote_addr, "ko", 86400)
|
||||
return false
|
||||
end
|
||||
|
||||
function M.check_reverse (reverse_list, resolvers)
|
||||
if #reverse_list > 0 then
|
||||
local rdns = dns.get_reverse(resolvers)
|
||||
if rdns ~= "" then
|
||||
local whitelisted = false
|
||||
for k, v in ipairs(reverse_list) do
|
||||
if rdns:sub(-#v) == v then
|
||||
whitelisted = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if whitelisted then
|
||||
local ips = dns.get_ips(rdns, resolvers)
|
||||
for k, v in ipairs(ips) do
|
||||
if v == ngx.var.remote_addr then
|
||||
ngx.shared.whitelist_reverse_cache:set(ngx.var.remote_addr, "ok", 86400)
|
||||
logger.log(ngx.NOTICE, "WHITELIST", "reverse " .. rdns .. " is in whitelist")
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
ngx.shared.whitelist_reverse_cache:set(ngx.var.remote_addr, "ko", 86400)
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user