Refactor ban functionality and improve ban listing

This commit is contained in:
Théophile Diot 2024-01-23 18:14:48 +01:00
parent 73c2ea42f0
commit a737bad334
No known key found for this signature in database
GPG key ID: 248FEA4BAE400D06
4 changed files with 71 additions and 22 deletions

View file

@ -33,7 +33,6 @@ local get_body_data = ngx_req.get_body_data
local get_body_file = ngx_req.get_body_file
local decode = cjson.decode
local encode = cjson.encode
local floor = math.floor
local match = string.match
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
@ -216,7 +215,26 @@ api.global.POST["^/ban$"] = function(self)
if not ok then
return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip)
end
datastore:set("bans_ip_" .. ip["ip"], "manual", ip["exp"])
local ban = {
ip = "",
exp = 86400,
reason = "manual",
}
ban.ip = ip["ip"]
if ip["exp"] then
ban.exp = ip["exp"]
end
if ip["reason"] then
ban.reason = ip["reason"]
end
datastore:set(
"bans_ip_" .. ban["ip"],
encode({
reason = ban["reason"],
date = os.time(),
}),
ban["exp"]
)
return self:response(HTTP_OK, "success", "ip " .. ip["ip"] .. " banned")
end
@ -224,12 +242,12 @@ api.global.GET["^/bans$"] = function(self)
local data = {}
for _, k in ipairs(datastore:keys()) do
if k:find("^bans_ip_") then
local reason, err = datastore:get(k)
local result, err = datastore:get(k)
if err then
return self:response(
HTTP_INTERNAL_SERVER_ERROR,
"error",
"can't access " .. k .. " from datastore : " .. reason
"can't access " .. k .. " from datastore : " .. result
)
end
local ok, ttl = datastore:ttl(k)
@ -240,7 +258,9 @@ api.global.GET["^/bans$"] = function(self)
"can't access ttl " .. k .. " from datastore : " .. ttl
)
end
local ban = { ip = k:sub(9, #k), reason = reason, exp = floor(ttl) }
local ban_data = decode(result)
local ban =
{ ip = k:sub(9, #k), reason = ban_data["reason"], date = ban_data["date"], exp = math.floor(ttl) }
table.insert(data, ban)
end
end

View file

@ -638,15 +638,16 @@ 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 result, err = datastore:get("bans_ip_" .. ip)
if not result and err ~= "not found" then
return nil, "datastore:get() error : " .. result
elseif result and err ~= "not found" then
local ok, ttl = datastore:ttl("bans_ip_" .. ip)
local ban_data = decode(result)
if not ok then
return true, reason, -1
return true, ban_data, -1
end
return true, reason, ttl
return true, ban_data, ttl
end
-- Redis case
local use_redis, err = utils.get_variable("USE_REDIS", false)
@ -701,7 +702,11 @@ end
utils.add_ban = function(ip, reason, ttl)
-- Set on local datastore
local ok, err = datastore:set("bans_ip_" .. ip, reason, ttl)
local ban_data = encode({
reason = reason,
date = os.time(),
})
local ok, err = datastore:set("bans_ip_" .. ip, ban_data, ttl)
if not ok then
return false, "datastore:set() error : " .. err
end
@ -719,7 +724,7 @@ utils.add_ban = function(ip, reason, ttl)
return false, "can't connect to redis server : " .. err
end
-- SET call
ok, err = clusterstore:call("set", "bans_ip_" .. ip, reason, "EX", ttl)
ok, err = clusterstore:call("set", "bans_ip_" .. ip, ban_data, "EX", ttl)
if not ok then
clusterstore:close()
return false, "redis SET failed : " .. err

View file

@ -1,5 +1,8 @@
#!/usr/bin/env python3
from datetime import datetime
from json import dumps, loads
from time import time
from dotenv import dotenv_values
from os import getenv, sep
from os.path import join
@ -19,10 +22,19 @@ from logger import setup_logger # type: ignore
def format_remaining_time(seconds):
days, seconds = divmod(seconds, 86400)
hours, seconds = divmod(seconds, 3600)
years, seconds = divmod(seconds, 60 * 60 * 24 * 365)
months, seconds = divmod(seconds, 60 * 60 * 24 * 30)
while months >= 12:
years += 1
months -= 12
days, seconds = divmod(seconds, 60 * 60 * 24)
hours, seconds = divmod(seconds, 60 * 60)
minutes, seconds = divmod(seconds, 60)
time_parts = []
if years > 0:
time_parts.append(f"{int(years)} year{'' if years == 1 else 's'}")
if months > 0:
time_parts.append(f"{int(months)} month{'' if months == 1 else 's'}")
if days > 0:
time_parts.append(f"{int(days)} day{'' if days == 1 else 's'}")
if hours > 0:
@ -206,14 +218,15 @@ class CLI(ApiCaller):
return True, f"IP {ip} has been unbanned"
return False, "error"
def ban(self, ip: str, exp: float) -> Tuple[bool, str]:
def ban(self, ip: str, exp: float, reason: str) -> Tuple[bool, str]:
if self.__redis:
ok = self.__redis.set(f"bans_ip_{ip}", "manual", ex=exp)
ok = self.__redis.set(f"bans_ip_{ip}", dumps({"reason": reason, "date": time()}))
if not ok:
self.__logger.error(f"Failed to ban {ip} in redis")
self.__redis.expire(f"bans_ip_{ip}", int(exp))
if self.send_to_apis("POST", "/ban", data={"ip": ip, "exp": exp}):
return (True, f"IP {ip} has been banned for {format_remaining_time(exp)}")
if self.send_to_apis("POST", "/ban", data={"ip": ip, "exp": exp, "reason": reason}):
return (True, f"IP {ip} has been banned for {format_remaining_time(exp)} with reason {reason}")
return False, "error"
def bans(self) -> Tuple[bool, str]:
@ -230,8 +243,13 @@ class CLI(ApiCaller):
servers["redis"] = []
for key in self.__redis.scan_iter("bans_ip_*"):
ip = key.decode("utf-8").replace("bans_ip_", "")
data = self.__redis.get(key)
if not data:
continue
exp = self.__redis.ttl(key)
servers["redis"].append({"ip": ip, "exp": exp, "reason": "manual"})
servers["redis"].append({"ip": ip, "exp": exp} | loads(data))
servers = {k: sorted(v, key=lambda x: x["date"]) for k, v in servers.items()}
cli_str = ""
for server, bans in servers.items():
@ -240,7 +258,7 @@ class CLI(ApiCaller):
cli_str += "No ban found\n"
for ban in bans:
cli_str += f"- {ban['ip']} for {format_remaining_time(ban['exp'])} : {ban.get('reason', 'no reason given')}\n"
cli_str += f"- {ban['ip']} ; banned the {datetime.fromtimestamp(ban['date']).strftime('%d-%m-%Y at %H:%M:%S')} for {format_remaining_time(ban['exp'])} remaining with reason \"{ban.get('reason', 'no reason given')}\"\n"
cli_str += "\n"
return True, cli_str

View file

@ -40,6 +40,12 @@ if __name__ == "__main__":
help=f"banning time in seconds (default : {ban_time})",
default=ban_time,
)
parser_ban.add_argument(
"-reason",
type=str,
help="reason for ban (default : manual)",
default="manual",
)
# Bans subparser
parser_bans = subparsers.add_parser("bans", help="list current bans")
@ -55,7 +61,7 @@ if __name__ == "__main__":
if args.command == "unban":
ret, err = cli.unban(args.ip)
elif args.command == "ban":
ret, err = cli.ban(args.ip, args.exp)
ret, err = cli.ban(args.ip, args.exp, args.reason)
elif args.command == "bans":
ret, err = cli.bans()