improved session management and add IP/UA checks

This commit is contained in:
florian 2023-05-20 17:46:35 +02:00
parent b5aaf62662
commit edd6e2ded5
No known key found for this signature in database
GPG Key ID: 3D80806F12602A7C
9 changed files with 218 additions and 83 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"

View File

@ -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