mirror of
https://github.com/bunkerity/bunkerized-nginx
synced 2023-12-13 21:30:18 +01:00
improved session management and add IP/UA checks
This commit is contained in:
parent
b5aaf62662
commit
edd6e2ded5
9 changed files with 218 additions and 83 deletions
|
@ -22,7 +22,7 @@ function plugin:initialize(id)
|
|||
local metadata = cjson.decode(encoded_metadata)
|
||||
local multisite = false
|
||||
local current_phase = ngx.get_phase()
|
||||
for i, check_phase in ipairs({ "set", "access", "log", "preread" }) do
|
||||
for i, check_phase in ipairs({ "set", "access", "content", "header", "log", "preread", "log_stream", "log_default" }) do
|
||||
if current_phase == check_phase then
|
||||
multisite = true
|
||||
break
|
||||
|
|
|
@ -12,6 +12,8 @@ local datastore = cdatastore:new()
|
|||
|
||||
local utils = {}
|
||||
|
||||
math.randomseed(os.time())
|
||||
|
||||
utils.get_variable = function(var, site_search)
|
||||
-- Default site search to true
|
||||
if site_search == nil then
|
||||
|
@ -363,7 +365,6 @@ utils.get_rdns = function(ip)
|
|||
for i, answer in ipairs(answers) do
|
||||
if answer.ptrdname then
|
||||
table.insert(ptrs, answer.ptrdname)
|
||||
logger:log(ngx.ERR, answer.ptrdname)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -510,22 +511,74 @@ utils.get_deny_status = function()
|
|||
return tonumber(status)
|
||||
end
|
||||
|
||||
utils.check_session = function()
|
||||
local _session, err, exists, refreshed = session.start({audience = "metadata"})
|
||||
if exists then
|
||||
for i, check in ipairs(ngx.ctx.bw.sessions_checks) do
|
||||
local key = check[1]
|
||||
local value = check[2]
|
||||
if _session:get(key) ~= value then
|
||||
local ok, err = _session:destroy()
|
||||
if not ok then
|
||||
_session:close()
|
||||
return false, "session:destroy() error : " .. err
|
||||
end
|
||||
logger:log(ngx.WARN, "session check " .. key .. " failed, destroying session")
|
||||
return utils.check_session()
|
||||
end
|
||||
end
|
||||
else
|
||||
for i, check in ipairs(ngx.ctx.bw.sessions_checks) do
|
||||
_session:set(check[1], check[2])
|
||||
end
|
||||
local ok, err = _session:save()
|
||||
if not ok then
|
||||
_session:close()
|
||||
return false, "session:save() error : " .. err
|
||||
end
|
||||
end
|
||||
ngx.ctx.bw.sessions_is_checked = true
|
||||
_session:close()
|
||||
return true, exists
|
||||
end
|
||||
|
||||
utils.get_session = function(audience)
|
||||
-- Session already in context
|
||||
if ngx.ctx.bw.session then
|
||||
ngx.ctx.bw.session:set_audience(audience)
|
||||
return ngx.ctx.bw.session
|
||||
-- Check session
|
||||
if not ngx.ctx.bw.sessions_is_checked then
|
||||
local ok, err = utils.check_session()
|
||||
if not ok then
|
||||
return false, "error while checking session, " .. err
|
||||
end
|
||||
end
|
||||
-- Open session and fill ctx
|
||||
local _session, err, exists, refreshed = session.start({ audience = audience })
|
||||
if err and err ~= "missing session cookie" and err ~= "no session" then
|
||||
logger:log(ngx.ERR, "session:start() error : " .. err)
|
||||
-- Open session with specific audience
|
||||
local _session, err, exists = session.open({audience = audience})
|
||||
if err then
|
||||
logger:log(ngx.INFO, "session:open() error : " .. err)
|
||||
end
|
||||
_session:set_audience(audience)
|
||||
ngx.ctx.bw.session = _session
|
||||
return _session
|
||||
end
|
||||
|
||||
utils.get_session_data = function(_session, site)
|
||||
local site_only = site == nil or site
|
||||
local data = _session:get_data()
|
||||
if site_only then
|
||||
return data[ngx.ctx.bw.server_name] or {}
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
utils.set_session_data = function(_session, data, site)
|
||||
local site_only = site == nil or site
|
||||
if site_only then
|
||||
local all_data = _session:get_data()
|
||||
all_data[ngx.ctx.bw.server_name] = data
|
||||
_session:set_data(all_data)
|
||||
return _session:save()
|
||||
end
|
||||
_session:set_data(data)
|
||||
return _session:save()
|
||||
end
|
||||
|
||||
utils.is_banned = function(ip)
|
||||
-- Check on local datastore
|
||||
local reason, err = datastore:get("bans_ip_" .. ip)
|
||||
|
|
|
@ -20,7 +20,7 @@ end
|
|||
|
||||
-- Remove previous data from the datastore
|
||||
logger:log(ngx.NOTICE, "deleting old keys from datastore ...")
|
||||
local data_keys = {"^plugin_", "^variable_", "^plugins$", "^api_", "^misc_"}
|
||||
local data_keys = {"^plugin", "^variable_", "^api_", "^misc_"}
|
||||
for i, key in pairs(data_keys) do
|
||||
local ok, err = datastore:delete_all(key)
|
||||
if not ok then
|
||||
|
|
|
@ -26,9 +26,15 @@ function antibot:access()
|
|||
return self:ret(true, "antibot not activated")
|
||||
end
|
||||
|
||||
-- Get session and data
|
||||
self.session = utils.get_session("antibot")
|
||||
self:get_session_data()
|
||||
-- Get session data
|
||||
local session, err = utils.get_session("antibot")
|
||||
if not session then
|
||||
return self:ret(false, "can't get session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
|
||||
end
|
||||
self.session = session
|
||||
self.session_data = utils.get_session_data(self.session)
|
||||
-- Check if session is valid
|
||||
self:check_session()
|
||||
|
||||
-- Don't go further if client resolved the challenge
|
||||
if self.session_data.resolved then
|
||||
|
@ -50,6 +56,11 @@ function antibot:access()
|
|||
return self:ret(true, "redirecting client to the challenge uri", nil, self.variables["ANTIBOT_URI"])
|
||||
end
|
||||
|
||||
-- Cookie case : don't display challenge page
|
||||
if self.session_data.resolved then
|
||||
return self:ret(true, "client already resolved the challenge", nil, self.session_data.original_uri)
|
||||
end
|
||||
|
||||
-- Display challenge needed
|
||||
if ngx.ctx.bw.request_method == "GET" then
|
||||
ngx.ctx.bw.antibot_display_content = true
|
||||
|
@ -89,13 +100,25 @@ function antibot:content()
|
|||
if self.variables["USE_ANTIBOT"] == "no" then
|
||||
return self:ret(true, "antibot not activated")
|
||||
end
|
||||
|
||||
-- Check if display content is needed
|
||||
if not ngx.ctx.bw.antibot_display_content then
|
||||
return self:ret(true, "display content not needed", nil, "/")
|
||||
end
|
||||
-- Get session and data
|
||||
self.session = utils.get_session("antibot")
|
||||
self:get_session_data(true)
|
||||
|
||||
-- Get session data
|
||||
local session, err = utils.get_session("antibot")
|
||||
if not session then
|
||||
return self:ret(false, "can't get session : " .. err, ngx.HTTP_INTERNAL_SERVER_ERROR)
|
||||
end
|
||||
self.session = session
|
||||
self.session_data = utils.get_session_data(self.session)
|
||||
|
||||
-- Direct access without session
|
||||
if not self.session_data.prepared then
|
||||
return self:ret(true, "no session", nil, "/")
|
||||
end
|
||||
|
||||
-- Display content
|
||||
local ok, err = self:display_challenge()
|
||||
if not ok then
|
||||
|
@ -104,50 +127,42 @@ function antibot:content()
|
|||
return self:ret(true, "content displayed")
|
||||
end
|
||||
|
||||
function antibot:get_session_data(no_check)
|
||||
local session_data = self.session:get_data()
|
||||
if session_data[ngx.ctx.bw.server_name] then
|
||||
local data = cjson.decode(session_data[ngx.ctx.bw.server_name])
|
||||
if no_check then
|
||||
self.session_data = data
|
||||
return
|
||||
end
|
||||
if not data.time_resolve and not data.time_valid then
|
||||
self.session_data = {}
|
||||
self.session_updated = true
|
||||
return
|
||||
end
|
||||
local time = ngx.now()
|
||||
self.session_data = data
|
||||
-- Check valid time
|
||||
if data.resolved and (data.time_valid > time or time - data.time_valid > tonumber(self.variables["ANTIBOT_TIME_VALID"])) then
|
||||
self.session_data.resolved = false
|
||||
self.session_data.prepared = false
|
||||
self.session_updated = true
|
||||
return
|
||||
end
|
||||
-- Check resolve time
|
||||
if not data.resolved and (data.time_resolve > time or time - data.time_resolve > tonumber(self.variables["ANTIBOT_TIME_RESOLVE"])) then
|
||||
self.session_data.prepared = false
|
||||
self.session_updated = true
|
||||
return
|
||||
end
|
||||
-- Session is valid
|
||||
function antibot:check_session()
|
||||
-- Get values
|
||||
local time_resolve = self.session_data.time_resolve
|
||||
local time_valid = self.session_data.time_valid
|
||||
-- Not resolved and not prepared
|
||||
if not time_resolve and not time_valid then
|
||||
self.session_data = {}
|
||||
self.session_updated = true
|
||||
return
|
||||
end
|
||||
-- Check if still valid
|
||||
local time = ngx.now()
|
||||
local resolved = self.session_data.resolved
|
||||
if resolved and (time_valid > time or time - time_valid > tonumber(self.variables["ANTIBOT_TIME_VALID"])) then
|
||||
self.session_data = {}
|
||||
self.session_updated = true
|
||||
return
|
||||
end
|
||||
-- Check if new prepare is needed
|
||||
if not resolved and (time_resolve > time or time - time_resolve > tonumber(self.variables["ANTIBOT_TIME_RESOLVE"])) then
|
||||
self.session_data = {}
|
||||
self.session_updated = true
|
||||
return
|
||||
end
|
||||
self.session_data = {}
|
||||
self.session_updated = true
|
||||
return
|
||||
end
|
||||
|
||||
function antibot:set_session_data()
|
||||
if self.session_updated then
|
||||
local session_data = self.session:get_data()
|
||||
session_data[ngx.ctx.bw.server_name] = cjson.encode(self.session_data)
|
||||
self.session:set_data(session_data)
|
||||
return self.session:save()
|
||||
local ok, err = utils.set_session_data(self.session, self.session_data)
|
||||
if not ok then
|
||||
return false, err
|
||||
end
|
||||
self.session_updated = false
|
||||
return true, "updated"
|
||||
end
|
||||
return true, "no updates"
|
||||
return true, "no update"
|
||||
end
|
||||
|
||||
function antibot:prepare_challenge()
|
||||
|
|
|
@ -267,20 +267,21 @@ function blacklist:is_blacklisted_ip()
|
|||
if ngx.ctx.bw.ip_is_global then
|
||||
local asn, err = utils.get_asn(ngx.ctx.bw.remote_addr)
|
||||
if not asn then
|
||||
return nil, "ASN " .. err
|
||||
end
|
||||
local ignore = false
|
||||
for i, ignore_asn in ipairs(self.lists["IGNORE_ASN"]) do
|
||||
if ignore_asn == tostring(asn) then
|
||||
ignore = true
|
||||
break
|
||||
self.logger:log(ngx.ERR, "can't get ASN of IP " .. ngx.ctx.bw.remote_addr .. " : " .. err)
|
||||
else
|
||||
local ignore = false
|
||||
for i, ignore_asn in ipairs(self.lists["IGNORE_ASN"]) do
|
||||
if ignore_asn == tostring(asn) then
|
||||
ignore = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Check if ASN is in blacklist
|
||||
if not ignore then
|
||||
for i, bl_asn in ipairs(self.lists["ASN"]) do
|
||||
if bl_asn == tostring(asn) then
|
||||
return true, "ASN " .. bl_asn
|
||||
-- Check if ASN is in blacklist
|
||||
if not ignore then
|
||||
for i, bl_asn in ipairs(self.lists["ASN"]) do
|
||||
if bl_asn == tostring(asn) then
|
||||
return true, "ASN " .. bl_asn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -216,11 +216,12 @@ function greylist:is_greylisted_ip()
|
|||
if ngx.ctx.bw.ip_is_global then
|
||||
local asn, err = utils.get_asn(ngx.ctx.bw.remote_addr)
|
||||
if not asn then
|
||||
return nil, "ASN " .. err
|
||||
end
|
||||
for i, bl_asn in ipairs(self.lists["ASN"]) do
|
||||
if bl_asn == tostring(asn) then
|
||||
return true, "ASN " .. bl_asn
|
||||
self.logger:log(ngx.ERR, "can't get ASN of IP " .. ngx.ctx.bw.remote_addr .. " : " .. err)
|
||||
else
|
||||
for i, bl_asn in ipairs(self.lists["ASN"]) do
|
||||
if bl_asn == tostring(asn) then
|
||||
return true, "ASN " .. bl_asn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -46,9 +46,27 @@
|
|||
"default": "86400",
|
||||
"help": "Maximum time (in seconds) before a session is destroyed.",
|
||||
"id": "sessions-absolute-timeout",
|
||||
"label": "SessionS absolute timeout",
|
||||
"label": "Sessions absolute timeout",
|
||||
"regex": "^\\d+$",
|
||||
"type": "text"
|
||||
},
|
||||
"SESSIONS_CHECK_IP": {
|
||||
"context": "global",
|
||||
"default": "yes",
|
||||
"help": "Destroy session if IP address is different than original one.",
|
||||
"id": "sessions-check-ip",
|
||||
"label": "Sessions check IP",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
},
|
||||
"SESSIONS_CHECK_USER_AGENT": {
|
||||
"context": "global",
|
||||
"default": "yes",
|
||||
"help": "Destroy session if User-Agent is different than original one.",
|
||||
"id": "sessions-user-agent",
|
||||
"label": "Sessions check User-Agent",
|
||||
"regex": "^(yes|no)$",
|
||||
"type": "check"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,37 @@ local sessions = class("sessions", plugin)
|
|||
function sessions:initialize()
|
||||
-- Call parent initialize
|
||||
plugin.initialize(self, "sessions")
|
||||
-- Check if random cookie name and secrets are already generated
|
||||
local is_random = {
|
||||
"SESSIONS_SECRET",
|
||||
"SESSIONS_NAME"
|
||||
}
|
||||
self.randoms = {}
|
||||
for i, var in ipairs(is_random) do
|
||||
if self.variables[var] == "random" then
|
||||
local data, err = self.datastore:get("storage_sessions_" .. var)
|
||||
if data then
|
||||
self.randoms[var] = data
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function sessions:set()
|
||||
if self.is_loading or self.kind ~= "http" then
|
||||
return self:ret(true, "set not needed")
|
||||
end
|
||||
local checks = {
|
||||
["IP"] = ngx.ctx.bw.remote_addr,
|
||||
["USER_AGENT"] = ngx.ctx.bw.http_user_agent or ""
|
||||
}
|
||||
ngx.ctx.bw.sessions_checks = {}
|
||||
for check, value in pairs(checks) do
|
||||
if self.variables["SESSIONS_CHECK_" .. check] == "yes" then
|
||||
table.insert(ngx.ctx.bw.sessions_checks, {check, value})
|
||||
end
|
||||
end
|
||||
return self:ret(true, "success")
|
||||
end
|
||||
|
||||
function sessions:init()
|
||||
|
@ -41,10 +72,26 @@ function sessions:init()
|
|||
absolute_timeout = tonumber(self.variables["SESSIONS_ABSOLUTE_TIMEOUT"])
|
||||
}
|
||||
if self.variables["SESSIONS_SECRET"] == "random" then
|
||||
config.secret = utils.rand(16)
|
||||
if self.randoms["SESSIONS_SECRET"] then
|
||||
config.secret = self.randoms["SESSIONS_SECRET"]
|
||||
else
|
||||
config.secret = utils.rand(16)
|
||||
local ok, err = self.datastore:set("storage_sessions_SESSIONS_SECRET", config.secret)
|
||||
if not ok then
|
||||
self.logger:log(ngx.ERR, "error from datastore:set : " .. err)
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.variables["SESSIONS_NAME"] == "random" then
|
||||
config.cookie_name = utils.rand(16)
|
||||
if self.randoms["SESSIONS_NAME"] then
|
||||
config.cookie_name = self.randoms["SESSIONS_NAME"]
|
||||
else
|
||||
config.cookie_name = utils.rand(16)
|
||||
local ok, err = self.datastore:set("storage_sessions_SESSIONS_NAME", config.cookie_name)
|
||||
if not ok then
|
||||
self.logger:log(ngx.ERR, "error from datastore:set : " .. err)
|
||||
end
|
||||
end
|
||||
end
|
||||
if redis_vars["USE_REDIS"] ~= "yes" then
|
||||
config.storage = "cookie"
|
||||
|
|
|
@ -271,7 +271,6 @@ function whitelist:is_whitelisted_ip()
|
|||
end
|
||||
end
|
||||
if forward_check then
|
||||
local forward_ok = false
|
||||
local ip_list, err = utils.get_ips(forward_check)
|
||||
if ip_list then
|
||||
for i, ip in ipairs(ip_list) do
|
||||
|
@ -293,11 +292,12 @@ function whitelist:is_whitelisted_ip()
|
|||
if ngx.ctx.bw.ip_is_global then
|
||||
local asn, err = utils.get_asn(ngx.ctx.bw.remote_addr)
|
||||
if not asn then
|
||||
return nil, "ASN " .. err
|
||||
end
|
||||
for i, bl_asn in ipairs(self.lists["ASN"]) do
|
||||
if bl_asn == tostring(asn) then
|
||||
return true, "ASN " .. bl_asn
|
||||
self.logger:log(ngx.ERR, "can't get ASN of IP " .. ngx.ctx.bw.remote_addr .. " : " .. err)
|
||||
else
|
||||
for i, bl_asn in ipairs(self.lists["ASN"]) do
|
||||
if bl_asn == tostring(asn) then
|
||||
return true, "ASN " .. bl_asn
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue