2489 lines
60 KiB
Lua
2489 lines
60 KiB
Lua
--[[
|
|
sban mod for Minetest designed and coded by shivajiva101@hotmail.com
|
|
|
|
request an insecure enviroment to load the db handler
|
|
and access files in the world folder. This requires
|
|
access via secure.trusted in the minetest.conf file
|
|
before it will work! For example:
|
|
|
|
secure.trusted = sban
|
|
|
|
]]
|
|
|
|
local ie = minetest.request_insecure_environment()
|
|
|
|
-- success?
|
|
if not ie then
|
|
error("insecure environment inaccessible" ..
|
|
" - make sure this mod has been added to the" ..
|
|
" secure.trusted setting in minetest.conf!")
|
|
end
|
|
|
|
local _sql = ie.require("lsqlite3")
|
|
|
|
-- secure this instance of sqlite3 global
|
|
if sqlite3 then sqlite3 = nil end
|
|
|
|
-- register admin privilege
|
|
minetest.register_privilege("ban_admin", {
|
|
description = "ban administrator",
|
|
give_to_singleplayer = false,
|
|
give_to_admin = true,
|
|
})
|
|
|
|
local WP = minetest.get_worldpath()
|
|
local WL -- whitelist cache
|
|
local ESC = minetest.formspec_escape
|
|
local FORMNAME = "sban:main"
|
|
local bans = {}
|
|
local name_cache = {}
|
|
local ip_cache = {}
|
|
local hotlist = {}
|
|
local failed = {}
|
|
local retry = 2 -- retries for save_failed
|
|
local delay = 5
|
|
local DB = WP.."/sban.sqlite"
|
|
local db_version = '0.2.1'
|
|
local db = _sql.open(DB) -- connection
|
|
local mod_version = '0.2.0'
|
|
local expiry, owner, owner_id, def_duration, display_max, names_per_id, api
|
|
local importer, ID, HL_Max, max_cache_records, ttl, cap, ip_limit
|
|
local formstate = {}
|
|
local t_units = {
|
|
s = 1, S=1, m = 60, h = 3600, H = 3600,
|
|
d = 86400, D = 86400, w = 604800, W = 604800,
|
|
M = 2592000, y = 31104000, Y = 31104000, [""] = 1
|
|
}
|
|
local createDb, tmp_db, tmp_final
|
|
sban = {} -- global table holds API functions
|
|
|
|
--[[
|
|
################
|
|
### Settings ###
|
|
################
|
|
]]
|
|
|
|
-- db
|
|
db:busy_timeout(50)
|
|
|
|
-- minetest.conf
|
|
if minetest.settings then
|
|
expiry = minetest.settings:get("sban.ban_max")
|
|
owner = minetest.settings:get("name")
|
|
def_duration = minetest.settings:get("sban.fs_duration") or "1w"
|
|
display_max = tonumber(minetest.settings:get("sban.display_max")) or 10
|
|
names_per_id = tonumber(minetest.settings:get("sban.accounts_per_id"))
|
|
ip_limit = tonumber(minetest.settings:get("sban.ip_limit"))
|
|
importer = minetest.settings:get_bool("sban.import_enabled", true)
|
|
HL_Max = tonumber(minetest.settings:get("sban.hotlist_max")) or 15
|
|
max_cache_records = tonumber(minetest.settings:get("sban.cache.max")) or 1000
|
|
ttl = tonumber(minetest.settings:get("sban.cache.ttl")) or 86400
|
|
api = minetest.settings:get_bool("sban.api", true)
|
|
else
|
|
-- old api method
|
|
expiry = minetest.setting_get("sban.ban_max")
|
|
owner = minetest.setting_get("name")
|
|
def_duration = minetest.setting_get("sban.fs_duration") or "1w"
|
|
display_max = tonumber(minetest.setting_get("sban.display_max")) or 10
|
|
names_per_id = tonumber(minetest.setting_get("sban.accounts_per_id"))
|
|
ip_limit = tonumber(minetest.setting_get("sban.ip_limit"))
|
|
importer = minetest.setting_getbool("sban.import_enable") or true
|
|
HL_Max = tonumber(minetest.setting_get("sban.hotlist_max")) or 15
|
|
max_cache_records = tonumber(minetest.setting_get("sban.cache_max")) or 1000
|
|
ttl = tonumber(minetest.setting_get("sban.cache_ttl")) or 86400
|
|
api = minetest.setting_getbool("sban.api") or true
|
|
end
|
|
|
|
--[[
|
|
######################
|
|
### DB callback ###
|
|
######################
|
|
]]
|
|
|
|
-- used for debugging, traces & logs sqlite3 actions
|
|
local dev = false
|
|
if dev then
|
|
db:trace(
|
|
function(ud, sql)
|
|
minetest.log("action", "Sqlite Trace: " .. sql)
|
|
end
|
|
)
|
|
|
|
-- Log the lines modified in the db
|
|
optbl = {
|
|
[_sql.UPDATE] = "UPDATE";
|
|
[_sql.INSERT] = "INSERT";
|
|
[_sql.DELETE] = "DELETE"
|
|
}
|
|
setmetatable(optbl,
|
|
{__index=function(t,n) return string.format("Unknown op %d",n) end})
|
|
|
|
udtbl = {0, 0, 0}
|
|
|
|
db:update_hook(
|
|
function(ud, op, dname, tname, rowid)
|
|
minetest.log("action", "[sban] " .. optbl[op] ..
|
|
" applied to db table " .. tname .. " on rowid " .. rowid)
|
|
end, udtbl
|
|
)
|
|
|
|
end
|
|
|
|
--[[
|
|
##########################
|
|
### Helper Functions ###
|
|
##########################
|
|
]]
|
|
|
|
-- Db wrapper for error reporting
|
|
-- @param stmt String containing SQL statements
|
|
-- @return Boolean and error message
|
|
local function db_exec(stmt)
|
|
if db:exec(stmt) ~= _sql.OK then
|
|
minetest.log("error", "[sban] Sqlite ERROR: "..db:errmsg())
|
|
return false, db:errmsg()
|
|
else
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Convert value to seconds (src: xban2)
|
|
-- @param t String containing alphanumerical duration
|
|
-- @return Integer seconds of duration
|
|
local function parse_time(str)
|
|
local s = 0
|
|
for n, u in str:gmatch("(%d+)([smhdwySHDWMY]?)") do
|
|
s = s + (tonumber(n) * (t_units[u] or 1))
|
|
end
|
|
return s
|
|
end
|
|
|
|
-- Convert UTC to human readable date format
|
|
-- @param utc_int Integer, seconds since epoch
|
|
-- @return String containing datetime
|
|
local function hrdf(utc_int)
|
|
if type(utc_int) == "number" then
|
|
return (utc_int and os.date("%c", utc_int))
|
|
end
|
|
end
|
|
|
|
-- Check if param is an ip address
|
|
-- @paran str String
|
|
-- @return true if ip else nil
|
|
local function is_ip(str)
|
|
if str:find(":") or str:find("%.") then
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Escape special chars in reason string
|
|
-- @param str String input
|
|
-- @return escaped string
|
|
local function escape_string(str)
|
|
-- handle apostrophes
|
|
return str:gsub("'", "''")
|
|
end
|
|
|
|
-- Format ip string
|
|
-- @param str String input
|
|
-- @return formatted String
|
|
local function ip_key(str)
|
|
-- strip point and colon chars to create a key
|
|
local res = str:gsub("%.", "")
|
|
res:gsub('%:', '')
|
|
return res
|
|
end
|
|
|
|
-- Increment db player id
|
|
-- @return nil
|
|
local function inc_id()
|
|
ID = ID + 1
|
|
return ID
|
|
end
|
|
|
|
-- Manage cache size
|
|
-- @return nil
|
|
local function trim_cache()
|
|
|
|
-- return if cache isn't full, nothing to do
|
|
if cap < max_cache_records then return end
|
|
|
|
local oldest = os.time() -- init
|
|
local name, id
|
|
|
|
-- iterate the name cache looking for the
|
|
-- oldest last_login value
|
|
for key, data in pairs(name_cache) do
|
|
if data.last_login < oldest then
|
|
oldest = data.last_login
|
|
name = key
|
|
id = data.id
|
|
end
|
|
end
|
|
|
|
-- remove entries in the ip cache
|
|
for k,v in pairs(ip_cache) do
|
|
if v == id then
|
|
ip_cache[k] = nil
|
|
end
|
|
end
|
|
|
|
-- remove name cache entry
|
|
name_cache[name] = nil
|
|
|
|
-- adjust the cap
|
|
cap = cap - 1
|
|
end
|
|
|
|
if importer then
|
|
|
|
createDb = [[
|
|
CREATE TABLE IF NOT EXISTS active (
|
|
id INTEGER(10) PRIMARY KEY,
|
|
name VARCHAR(50),
|
|
source VARCHAR(50),
|
|
created INTEGER(30),
|
|
reason VARCHAR(300),
|
|
expires INTEGER(30),
|
|
pos VARCHAR(50)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS expired (
|
|
id INTEGER(10),
|
|
name VARCHAR(50),
|
|
source VARCHAR(50),
|
|
created INTEGER(30),
|
|
reason VARCHAR(300),
|
|
expires INTEGER(30),
|
|
u_source VARCHAR(50),
|
|
u_reason VARCHAR(300),
|
|
u_date INTEGER(30),
|
|
last_pos VARCHAR(50)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_expired_id ON expired(id);
|
|
|
|
CREATE TABLE IF NOT EXISTS name (
|
|
id INTEGER(10),
|
|
name VARCHAR(50) PRIMARY KEY,
|
|
created INTEGER(30),
|
|
last_login INTEGER(30),
|
|
login_count INTEGER(8) DEFAULT(1)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_name_id ON name(id);
|
|
CREATE INDEX IF NOT EXISTS idx_name_lastlogin ON name(last_login);
|
|
|
|
CREATE TABLE IF NOT EXISTS address (
|
|
id INTEGER(10),
|
|
ip VARCHAR(50) PRIMARY KEY,
|
|
created INTEGER(30),
|
|
last_login INTEGER(30),
|
|
login_count INTEGER(8) DEFAULT(1),
|
|
violation BOOLEAN
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_address_id ON address(id);
|
|
CREATE INDEX IF NOT EXISTS idx_address_lastlogin ON address(last_login);
|
|
|
|
CREATE TABLE IF NOT EXISTS whitelist (
|
|
name_or_ip VARCHAR(50) PRIMARY KEY,
|
|
source VARCHAR(50),
|
|
created INTEGER(30)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
setting VARCHAR(28) PRIMARY KEY,
|
|
data VARCHAR(255)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS violation (
|
|
id INTEGER PRIMARY KEY,
|
|
data VARCHAR
|
|
);
|
|
|
|
]]
|
|
db_exec(createDb)
|
|
|
|
tmp_db = [[
|
|
CREATE TABLE IF NOT EXISTS tmp_a (
|
|
id INTEGER(10),
|
|
name VARCHAR(50),
|
|
created INTEGER(30),
|
|
last_login INTEGER(30),
|
|
login_count INTEGER(8) DEFAULT(1)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tmp_b (
|
|
id INTEGER(10),
|
|
ip VARCHAR(50),
|
|
created INTEGER(30),
|
|
last_login INTEGER(30),
|
|
login_count INTEGER(8) DEFAULT(1),
|
|
violation BOOLEAN
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tmp_c (
|
|
id INTEGER(10),
|
|
name VARCHAR(50),
|
|
source VARCHAR(50),
|
|
created INTEGER(30),
|
|
reason VARCHAR(300),
|
|
expires INTEGER(30),
|
|
pos VARCHAR(50)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tmp_d (
|
|
id INTEGER(10),
|
|
name VARCHAR(50),
|
|
source VARCHAR(50),
|
|
created INTEGER(30),
|
|
reason VARCHAR(300),
|
|
expires INTEGER(30),
|
|
u_source VARCHAR(50),
|
|
u_reason VARCHAR(300),
|
|
u_date INTEGER(30),
|
|
last_pos VARCHAR(50)
|
|
);
|
|
|
|
]]
|
|
|
|
tmp_final = [[
|
|
|
|
DELETE FROM tmp_a where rowid NOT IN (SELECT min(rowid) FROM tmp_a GROUP BY name);
|
|
DELETE FROM tmp_b where rowid NOT IN (SELECT min(rowid) FROM tmp_b GROUP BY ip);
|
|
DELETE FROM tmp_c where rowid NOT IN (SELECT min(rowid) FROM tmp_c GROUP BY id);
|
|
|
|
INSERT INTO name (id, name, created, last_login, login_count)
|
|
SELECT * FROM tmp_a WHERE tmp_a.name NOT IN (SELECT name FROM name);
|
|
|
|
INSERT INTO address(id, ip, created, last_login, login_count, violation)
|
|
SELECT * FROM tmp_b WHERE tmp_b.ip NOT IN (SELECT ip FROM address);
|
|
|
|
INSERT INTO active (id, name, source, created, reason, expires, pos)
|
|
SELECT * FROM tmp_c WHERE tmp_c.id NOT IN (SELECT id FROM active);
|
|
|
|
INSERT INTO expired (id, name, source,created, reason, expires, u_source, u_reason, u_date, last_pos)
|
|
SELECT * FROM tmp_d;
|
|
|
|
DROP TABLE tmp_a;
|
|
DROP TABLE tmp_b;
|
|
DROP TABLE tmp_c;
|
|
DROP TABLE tmp_d;
|
|
|
|
COMMIT;
|
|
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
VACUUM;
|
|
]]
|
|
end
|
|
|
|
--[[
|
|
###########################
|
|
### Database: Queries ###
|
|
###########################
|
|
]]
|
|
|
|
-- Fetch an id for an ip or name
|
|
-- @param name_or_ip string
|
|
-- @returns id integer or nil
|
|
local function get_id(name_or_ip)
|
|
local q
|
|
if is_ip(name_or_ip) then
|
|
-- check cache first
|
|
if ip_cache[ip_key(name_or_ip)] then
|
|
return ip_cache[ip_key(name_or_ip)]
|
|
end
|
|
-- check db
|
|
q = ([[
|
|
SELECT id
|
|
FROM address
|
|
WHERE ip = '%s' LIMIT 1;]]
|
|
):format(name_or_ip)
|
|
else
|
|
-- check cache first
|
|
if name_cache[name_or_ip] then
|
|
return name_cache[name_or_ip].id
|
|
end
|
|
-- check db
|
|
q = ([[
|
|
SELECT id
|
|
FROM name
|
|
WHERE name = '%s' LIMIT 1;]]
|
|
):format(name_or_ip)
|
|
end
|
|
local it, state = db:nrows(q)
|
|
local row = it(state)
|
|
if row then
|
|
return row.id
|
|
end
|
|
end
|
|
|
|
-- Fetch last id from the name table
|
|
-- @return last id integer or nil
|
|
local function last_id()
|
|
local q = "SELECT MAX(id) AS id FROM name;"
|
|
local it, state = db:nrows(q)
|
|
local row = it(state)
|
|
if row then
|
|
return row.id
|
|
end
|
|
end
|
|
|
|
-- Fetch expired ban records
|
|
-- @param id integer
|
|
-- @return ipair table of expired ban records
|
|
local function player_ban_expired(id)
|
|
local r = {}
|
|
local q = ([[
|
|
SELECT * FROM expired WHERE id = %i;
|
|
]]):format(id)
|
|
for row in db:nrows(q) do
|
|
r[#r + 1] = row
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- Fetch name records
|
|
-- @param id integer
|
|
-- @return ipair table of name records ordered by last login
|
|
local function name_records(id)
|
|
local r = {}
|
|
local q = ([[
|
|
SELECT * FROM name
|
|
WHERE id = %i ORDER BY last_login DESC;
|
|
]]):format(id)
|
|
for row in db:nrows(q) do
|
|
r[#r + 1] = row
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- Fetch address records
|
|
-- @param id integer
|
|
-- @return ipair table of ip address records ordered by last login
|
|
local function address_records(id)
|
|
local r = {}
|
|
local q = ([[
|
|
SELECT * FROM address
|
|
WHERE id = %i ORDER BY last_login DESC;
|
|
]]):format(id)
|
|
for row in db:nrows(q) do
|
|
r[#r + 1] = row
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- Fetch violation records
|
|
-- @param id integer
|
|
-- @return ipair table of violation records
|
|
local function violation_record(id)
|
|
local q = ([[
|
|
SELECT data FROM violation WHERE id = %i LIMIT 1;
|
|
]]):format(id)
|
|
local it, state = db:nrows(q)
|
|
local row = it(state)
|
|
if row then
|
|
return minetest.deserialize(row.data)
|
|
end
|
|
end
|
|
|
|
-- Fetch active bans
|
|
-- @return keypair table
|
|
local function get_active_bans()
|
|
local r = {}
|
|
local q = "SELECT * FROM active;"
|
|
for row in db:nrows(q) do
|
|
r[row.id] = row
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- Fetch whitelist
|
|
-- @return keypair table
|
|
local function get_whitelist()
|
|
local r = {}
|
|
local q = "SELECT * FROM whitelist;"
|
|
for row in db:nrows(q) do
|
|
r[row.name_or_ip] = true
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- Fetch config setting
|
|
-- @param name setting string
|
|
-- @return data string
|
|
local function get_setting(name)
|
|
local q = ([[SELECT data FROM config WHERE setting = '%s';]]):format(name)
|
|
local it, state = db:nrows(q)
|
|
local row = it(state)
|
|
if row then
|
|
return row.data
|
|
end
|
|
end
|
|
|
|
-- Fetch names like 'name'
|
|
-- @param name string
|
|
-- @return keypair table of names
|
|
local function get_names(name)
|
|
local r,t = {},{}
|
|
local q = "SELECT name FROM name WHERE name LIKE '%"..name.."%';"
|
|
for row in db:nrows(q) do
|
|
-- Simple sort using a temp table to remove duplicates
|
|
if not t[row.name] then
|
|
r[#r+1] = row.name
|
|
t[row.name] = true
|
|
end
|
|
end
|
|
return r
|
|
end
|
|
|
|
cap = 0
|
|
-- Build name and address cache
|
|
-- @return nil
|
|
local function build_cache()
|
|
-- get last login timestamp
|
|
local q = "SELECT max(last_login) AS login FROM name;"
|
|
local it, state = db:nrows(q)
|
|
local last = it(state)
|
|
if last.login then
|
|
last = last.login - ttl -- adjust
|
|
q = ([[
|
|
SELECT * FROM name WHERE last_login > %i
|
|
ORDER BY last_login ASC LIMIT %s;
|
|
]]):format(last, max_cache_records)
|
|
for row in db:nrows(q) do
|
|
name_cache[row.name] = row
|
|
cap = cap + 1
|
|
end
|
|
minetest.log("action", "[sban] caching " .. cap .. " name records")
|
|
local ctr = 0
|
|
for k, row in pairs(name_cache) do
|
|
for _,v in ipairs(address_records(row.id)) do
|
|
ip_cache[ip_key(v.ip)] = row.id
|
|
ctr = ctr + 1
|
|
end
|
|
end
|
|
minetest.log("action", "[sban] caching " .. ctr .. " ip records")
|
|
end
|
|
end
|
|
build_cache()
|
|
|
|
--[[
|
|
###########################
|
|
### Database: Inserts ###
|
|
###########################
|
|
]]
|
|
|
|
-- Create name record
|
|
-- @param id integer
|
|
-- @param name string
|
|
-- @param ts integer utc
|
|
-- @return nil
|
|
local function add_name_record(id, name, ts)
|
|
local stmt = ([[
|
|
INSERT INTO name (id,name,created,last_login,login_count)
|
|
VALUES (%i,'%s',%i,%i,1);
|
|
]]):format(id, name, ts, ts)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Create ip record
|
|
-- @param id integer
|
|
-- @param ip string
|
|
-- @param ts integer os.time()
|
|
-- @return nil
|
|
local function add_ip_record(id, ip, ts)
|
|
local stmt = ([[
|
|
INSERT INTO address (
|
|
id,
|
|
ip,
|
|
created,
|
|
last_login,
|
|
login_count,
|
|
violation
|
|
) VALUES (%i,'%s',%i,%i,1,0);
|
|
]]):format(id, ip, ts, ts)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Create and cache id record
|
|
-- @param id integer
|
|
-- @param name string
|
|
-- @param ts integer
|
|
-- @param ip string
|
|
-- @return res bool, error string
|
|
local function create_player_record(id, name, ts, ip)
|
|
local stmt = ([[
|
|
BEGIN TRANSACTION;
|
|
INSERT INTO name (
|
|
id,
|
|
name,
|
|
created,
|
|
last_login,
|
|
login_count
|
|
) VALUES (%i,'%s',%i,%i,1);
|
|
INSERT INTO address (
|
|
id,
|
|
ip,
|
|
created,
|
|
last_login,
|
|
login_count,
|
|
violation
|
|
) VALUES (%i,'%s',%i,%i,1,0);
|
|
COMMIT;
|
|
]]):format(id,name,ts,ts,id,ip,ts,ts)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Create db whitelist record
|
|
-- @param source name string
|
|
-- @param name_or_ip string
|
|
-- @return res bool, error msg
|
|
local function add_whitelist_record(source, name_or_ip)
|
|
local ts = os.time()
|
|
local stmt = ([[
|
|
INSERT INTO whitelist
|
|
VALUES ('%s', '%s', %i)
|
|
]]):format(name_or_ip, source, ts)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Create db ban record
|
|
-- @param id integer
|
|
-- @param name string
|
|
-- @param source string
|
|
-- @param created string
|
|
-- @param reason string
|
|
-- @param expires integer
|
|
-- @param pos string
|
|
-- @return res bool, error string
|
|
local function create_ban_record(id, name, source, created, reason, expires, pos)
|
|
local stmt = ([[
|
|
INSERT INTO active VALUES (%i,'%s','%s',%i,'%s',%i,'%s');
|
|
]]):format(id, name, source, created, reason, expires, pos)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- initialise db setting
|
|
-- @param setting string
|
|
-- @param data string
|
|
-- @return res bool, error string
|
|
local function init_setting(setting, data)
|
|
local stmt = ([[
|
|
INSERT INTO config VALUES ('%s', '%s');
|
|
]]):format(setting, data)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
--[[
|
|
###########################
|
|
### Database: Updates ###
|
|
###########################
|
|
]]
|
|
|
|
-- Update login record
|
|
-- @param id integer
|
|
-- @param name string
|
|
-- @return bool
|
|
-- @return string on error
|
|
local function update_login_record(id, name, ts)
|
|
-- update Db name record
|
|
local stmt = ([[
|
|
UPDATE name SET
|
|
last_login = %i,
|
|
login_count = login_count + 1
|
|
WHERE name = '%s';
|
|
]]):format(ts, name)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Update address record
|
|
-- @param id integer
|
|
-- @param ip string
|
|
-- @return bool
|
|
-- @return string on error
|
|
local function update_address_record(id, ip, ts)
|
|
local stmt = ([[
|
|
UPDATE address
|
|
SET
|
|
last_login = %i,
|
|
login_count = login_count + 1
|
|
WHERE id = %i AND ip = '%s';
|
|
]]):format(ts, id, ip)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Update ban record
|
|
-- @param id integer
|
|
-- @param source name string
|
|
-- @param reason string
|
|
-- @param name string
|
|
-- @return bool
|
|
-- @return string on error
|
|
local function update_ban_record(id, source, reason, name)
|
|
local ts = os.time()
|
|
local row = bans[id] -- use cached data
|
|
local stmt = ([[
|
|
INSERT INTO expired VALUES (%i,'%s','%s',%i,'%s',%i,'%s','%s',%i,'%s');
|
|
DELETE FROM active WHERE id = %i;
|
|
]]):format(row.id, row.name, row.source, row.created, escape_string(row.reason),
|
|
row.expires, source, reason, ts, row.pos, row.id)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Update violation status
|
|
-- @param ip string
|
|
-- @return nil
|
|
local function update_idv_status(ip)
|
|
local stmt = ([[
|
|
UPDATE address
|
|
SET
|
|
violation = 'true'
|
|
WHERE ip = '%s';
|
|
]]):format(ip)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
--[[
|
|
##################################
|
|
### Database: Delete Records ###
|
|
##################################
|
|
]]
|
|
|
|
-- Remove ban record
|
|
-- @param id integer
|
|
-- @return bool & string on error
|
|
local function del_ban_record(id)
|
|
local stmt = ([[
|
|
DELETE FROM active WHERE id = %i
|
|
]]):format(id)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
-- Remove whitelist entry
|
|
-- @param name_or_ip string
|
|
-- @return nil
|
|
local function del_whitelist_record(name_or_ip)
|
|
local stmt = ([[
|
|
DELETE FROM whitelist WHERE name_or_ip = '%s'
|
|
]]):format(name_or_ip)
|
|
return db_exec(stmt)
|
|
end
|
|
|
|
--[[
|
|
###################
|
|
### Functions ###
|
|
###################
|
|
]]
|
|
|
|
-- Kicks players by name or id
|
|
-- @param name_or_id string or integer
|
|
-- @return nil
|
|
local function kicker(name_or_id, msg)
|
|
local r
|
|
if type(name_or_id) == "number" then
|
|
r = name_records(name_or_id)
|
|
elseif type(name_or_id) == "string" then
|
|
local id = get_id(name_or_id)
|
|
if id then r = name_records(id) end
|
|
end
|
|
if r == {} then return end
|
|
for i, v in ipairs(r) do
|
|
local player = minetest.get_player_by_name(v.name)
|
|
if player then
|
|
-- defeat entity attached bypass mechanism
|
|
player:set_detach()
|
|
minetest.kick_player(v.name, msg)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Create and cache name record
|
|
-- @param id integer
|
|
-- @param name string
|
|
-- @return nil
|
|
local function add_name(id, name)
|
|
local ts = os.time()
|
|
local res = add_name_record(id, name, ts)
|
|
if res then
|
|
-- cache name record
|
|
name_cache[name] = {
|
|
id = id,
|
|
name = name,
|
|
last_login = ts,
|
|
login_count = 1
|
|
}
|
|
else
|
|
minetest.log("warning", ([[[sban] Failed to add %s with id %i
|
|
to the name table]]):format(name, id))
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Create and cache ip record
|
|
-- @param id integer
|
|
-- @param ip string
|
|
-- @return nil
|
|
local function add_ip(id, ip)
|
|
local ts = os.time()
|
|
local res = add_ip_record(id, ip, ts)
|
|
if res then
|
|
ip_cache[ip_key(ip)] = id -- cache
|
|
else
|
|
minetest.log("action", ([[[sban] Failed to add %s with id %i
|
|
to the address table]]):format(ip, id))
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Create and cache id record
|
|
-- @param name string
|
|
-- @param ip string
|
|
-- @return nil, -1 or id
|
|
local function create_player(name, ip)
|
|
local ts, id, res, err
|
|
if failed[name] then
|
|
ts = failed[name].ts
|
|
id = failed[name].id
|
|
else
|
|
ts = os.time()
|
|
id = inc_id()
|
|
end
|
|
res, err = create_player_record(id, name, ts, ip)
|
|
if err and failed[name] == nil then
|
|
-- create job in queue
|
|
failed[name] = {
|
|
id = id, -- assigned id
|
|
ip = ip, -- ip address
|
|
ts = ts, -- timestamp
|
|
jt = 1, -- job type
|
|
n = 0 -- retry counter
|
|
}
|
|
elseif err and failed[name] then
|
|
return -1
|
|
elseif res then
|
|
-- success, cache name record
|
|
name_cache[name] = {
|
|
id = id,
|
|
name = name,
|
|
last_login = ts
|
|
}
|
|
return id
|
|
end
|
|
end
|
|
|
|
-- Create ban record
|
|
-- @param name string
|
|
-- @param source string
|
|
-- @param reason string
|
|
-- @param expires integer
|
|
-- @return bool
|
|
local function create_ban(name, source, reason, expires)
|
|
|
|
local ts = os.time()
|
|
local id = get_id(name)
|
|
local player = minetest.get_player_by_name(name)
|
|
local msg
|
|
|
|
reason = escape_string(reason)
|
|
expires = expires or 0
|
|
|
|
-- initialise last position & fetch if playing
|
|
local pos = ""
|
|
if player then
|
|
pos = minetest.pos_to_string(vector.round(player:getpos()))
|
|
end
|
|
|
|
local res = create_ban_record(id, name, source, ts, reason, expires, pos)
|
|
if res then
|
|
-- cache the ban
|
|
bans[id] = {
|
|
id = id,
|
|
name = name,
|
|
source = source,
|
|
created = ts,
|
|
reason = reason,
|
|
expires = expires,
|
|
last_pos = pos
|
|
}
|
|
msg = "Owner kick bypass triggered in create_ban_record"
|
|
|
|
-- owner cannot be kicked ;)
|
|
if not dev and owner_id == id then
|
|
minetest.log("action", msg)
|
|
return res
|
|
end
|
|
|
|
-- create kick & log messages
|
|
local msg_k, msg_l
|
|
if expires ~= 0 then
|
|
-- temporary ban
|
|
local date = hrdf(expires)
|
|
msg_k = ("Banned: Expires: %s, Reason: %s"):format(date, reason)
|
|
msg_l = ("[sban] %s temp banned by %s reason: %s"
|
|
):format(name, source, reason) -- log msg
|
|
else
|
|
msg_k = ("Banned: Reason: %s"):format(reason)
|
|
msg_l = ("[sban] %s banned by %s reason: %s"
|
|
):format(name, source, reason)
|
|
end
|
|
minetest.log("action", msg_l)
|
|
|
|
kicker(id, msg_k)
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Update player ban
|
|
-- @param id integer
|
|
-- @param source name string
|
|
-- @param reason string
|
|
-- @param name string
|
|
-- @return bool
|
|
local function update_ban(id, source, reason, name)
|
|
reason = escape_string(reason)
|
|
local res = update_ban_record(id, source, reason, name)
|
|
if res then
|
|
bans[id] = nil -- update cache
|
|
-- log event
|
|
minetest.log("action",
|
|
("[sban] %s unbanned by %s reason: %s"):format(name, source, reason))
|
|
else
|
|
minetest.log("warning",
|
|
("[sban] record failed to save when banning %s"):format(name))
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Delete active ban by id
|
|
-- @param id integer
|
|
-- @return bool
|
|
local function del_ban(id)
|
|
local res = del_ban_record(id)
|
|
if res then
|
|
bans[id] = nil -- update cache
|
|
else
|
|
minetest.log("warning", ([[[sban] Failed to delete ban for id %i
|
|
in the active table]]):format(id))
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Update login record
|
|
-- @param id integer
|
|
-- @param name string
|
|
-- @return bool
|
|
local function update_login(id, name)
|
|
local ts = os.time()
|
|
local res = update_login_record(id, name, ts)
|
|
if res then
|
|
if not name_cache[name] then
|
|
name_cache[name] = {
|
|
id = id,
|
|
name = name,
|
|
last_login = ts
|
|
}
|
|
else
|
|
name_cache[name].last_login = ts
|
|
end
|
|
else
|
|
minetest.log("warning", ([[[sban] Failed to update login for %s with id %i
|
|
in the name table]]):format(name, id))
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Update ip address record
|
|
-- @param id integer
|
|
-- @param ip string
|
|
-- @return bool
|
|
local function update_address(id, ip)
|
|
local ts = os.time()
|
|
local res = update_address_record(id, ip, ts)
|
|
if not res then
|
|
minetest.log("action", ([[[sban] Failed to update address for %s with id %i
|
|
in the address table]]):format(ip, id))
|
|
end
|
|
return res
|
|
end
|
|
|
|
-- Create ip violation record
|
|
-- @param src_id integer
|
|
-- @param target_id integer
|
|
-- @param ip string
|
|
-- @return res bool, error string
|
|
local function manage_idv(src_id, target_id, ip)
|
|
local ts = os.time()
|
|
local stmt
|
|
local record = violation_record(src_id)
|
|
if record then
|
|
local idx
|
|
for i,v in ipairs(record) do
|
|
if v.id == target_id and v.ip == ip then
|
|
idx = i
|
|
break
|
|
end
|
|
end
|
|
if idx then
|
|
-- update record
|
|
record[idx].ctr = record[idx].ctr + 1
|
|
record[idx].last_login = ts
|
|
else
|
|
-- add record
|
|
record[#record+1] = {
|
|
id = target_id,
|
|
ip = ip,
|
|
ctr = 1,
|
|
created = ts,
|
|
last_login = ts
|
|
}
|
|
end
|
|
stmt = ([[
|
|
UPDATE violation SET data = '%s' WHERE id = %i;
|
|
]]):format(minetest.serialize(record), src_id)
|
|
else
|
|
record = {
|
|
id = target_id,
|
|
ip = ip,
|
|
ctr = 1,
|
|
created = ts,
|
|
last_login = ts
|
|
}
|
|
stmt = ([[
|
|
INSERT INTO violation VALUES (%i,'%s')
|
|
]]):format(src_id, minetest.serialize(record))
|
|
end
|
|
local res, err = db_exec(stmt)
|
|
return res, err
|
|
end
|
|
|
|
-- Display player data on the console
|
|
-- @param caller name string
|
|
-- @param target name string
|
|
-- @return nil
|
|
local function display_record(caller, target)
|
|
|
|
local id = get_id(target)
|
|
local r = name_records(id)
|
|
local bld = {}
|
|
|
|
if not r then
|
|
minetest.chat_send_player(caller, "No records for "..target)
|
|
return
|
|
end
|
|
|
|
-- Show names
|
|
local names = {}
|
|
for i,v in ipairs(r) do
|
|
table.insert(names, v.name)
|
|
end
|
|
bld[#bld+1] = minetest.colorize("#00FFFF", "[sban] records for: ") .. target
|
|
bld[#bld+1] = minetest.colorize("#00FFFF", "Names: ") .. table.concat(names, ", ")
|
|
|
|
local privs = minetest.get_player_privs(caller)
|
|
|
|
-- records loaded, display
|
|
local idx = 1
|
|
if #r > display_max then
|
|
idx = #r - display_max
|
|
bld[#bld+1] = minetest.colorize("#00FFFF", "Name records: ")..#r..
|
|
minetest.colorize("#00FFFF", " (showing last ")..display_max..
|
|
minetest.colorize("#00FFFF", " records)")
|
|
else
|
|
bld[#bld+1] = minetest.colorize("#00FFFF", "Name records: ")..#r
|
|
end
|
|
for i = idx, #r do
|
|
local d1 = hrdf(r[i].created)
|
|
local d2 = hrdf(r[i].last_login)
|
|
bld[#bld+1] = (minetest.colorize("#FFC000",
|
|
"[%s]").." Name: %s Created: %s Last login: %s"):format(i, r[i].name, d1, d2)
|
|
end
|
|
|
|
if privs.ban_admin == true then
|
|
r = address_records(id)
|
|
if #r > display_max then
|
|
idx = #r - display_max
|
|
bld[#bld+1] = minetest.colorize("#0FF", "IP records: ") .. #r ..
|
|
minetest.colorize("#0FF", " (showing last ") .. display_max ..
|
|
minetest.colorize("#0FF", " records)")
|
|
else
|
|
bld[#bld+1] = minetest.colorize("#0FF", "IP records: ") .. #r
|
|
idx = 1
|
|
end
|
|
for i = idx, #r do
|
|
-- format utc values
|
|
local d = hrdf(r[i].created)
|
|
bld[#bld+1] = (minetest.colorize("#FFC000", "[%s] ")..
|
|
"IP: %s Created: %s"):format(i, r[i].ip, d)
|
|
end
|
|
r = violation_record(id)
|
|
if r then
|
|
bld[#bld+1] = minetest.colorize("#0FF", "\nViolation records: ") .. #r
|
|
for i,v in ipairs(r) do
|
|
bld[#bld+1] = ("[%s] ID: %s IP: %s Created: %s Last login: %s"):format(
|
|
i, v.id, v.ip, hrdf(v.created), hrdf(v.last_login))
|
|
end
|
|
else
|
|
bld[#bld+1] = minetest.colorize("#0FF", "No violation records for ") .. target
|
|
end
|
|
end
|
|
|
|
r = player_ban_expired(id) or {}
|
|
bld[#bld+1] = minetest.colorize("#0FF", "Ban records:")
|
|
if #r > 0 then
|
|
|
|
bld[#bld+1] = minetest.colorize("#0FF", "Expired records: ")..#r
|
|
|
|
for i, e in ipairs(r) do
|
|
local d1 = hrdf(e.created)
|
|
local expires = "never"
|
|
if type(e.expires) == "number" and e.expires > 0 then
|
|
expires = hrdf(e.expires)
|
|
end
|
|
local d2 = hrdf(e.u_date)
|
|
bld[#bld+1] = (minetest.colorize("#FFC000", "[%s]")..
|
|
" Name: %s Created: %s Banned by: %s Reason: %s Expires: %s "
|
|
):format(i, e.name, d1, e.source, e.reason, expires) ..
|
|
("Unbanned by: %s Reason: %s Time: %s"):format(e.u_source, e.u_reason, d2)
|
|
end
|
|
|
|
else
|
|
bld[#bld+1] = "No expired ban records!"
|
|
end
|
|
|
|
r = bans[id]
|
|
local ban = tostring(r ~= nil)
|
|
bld[#bld+1] = minetest.colorize("#0FF", "Current Ban Status:")
|
|
if ban == 'true' then
|
|
local expires = "never"
|
|
local d = hrdf(r.created)
|
|
if type(r.expires) == "number" and r.expires > 0 then
|
|
expires = hrdf(r.expires)
|
|
end
|
|
bld[#bld+1] = ("Name: %s Created: %s Banned by: %s Reason: %s Expires: %s"
|
|
):format(r.name, d, r.source, r.reason, expires)
|
|
else
|
|
bld[#bld+1] = "no active ban record!"
|
|
end
|
|
bld[#bld+1] = minetest.colorize("#0FF", "Banned: ")..ban
|
|
return table.concat(bld, "\n")
|
|
end
|
|
|
|
-- Add an entry to and manage size of hotlist
|
|
-- @param name string
|
|
-- @return nil
|
|
local function manage_hotlist(name)
|
|
for _, v in ipairs(hotlist) do
|
|
if v == name then
|
|
-- no duplicates
|
|
return
|
|
end
|
|
end
|
|
-- fifo
|
|
table.insert(hotlist, name)
|
|
if #hotlist > HL_Max then
|
|
table.remove(hotlist, 1)
|
|
end
|
|
end
|
|
|
|
-- Save failed db inserts, limited to retry value
|
|
-- @return nil
|
|
local function save_failed()
|
|
for key,val in pairs(failed) do
|
|
if val.jt == 1 then -- job type: create player record
|
|
local res = create_player(key, val.ip)
|
|
if res == val.id then -- matching id passed back
|
|
minetest.log("action", ([[[sban] player record successfully
|
|
saved for %s with id %i after %i attempts!]]
|
|
):format(key,val.id,val.n))
|
|
failed[key] = nil -- remove
|
|
elseif res == -1 then -- failed
|
|
val.n = val.n + 1
|
|
if val.n > retry then
|
|
minetest.log("action", ([[[sban] player record failed to
|
|
save for %s with id %i after %i attempts!]]
|
|
):format(key,val.id,val.n))
|
|
failed[key] = nil -- remove from queue
|
|
-- kick player with a rejoin message
|
|
-- no point having a player connected
|
|
-- without a db record
|
|
local msg = "There was a problem, please rejoin the server..."
|
|
local player = minetest.get_player_by_name(key)
|
|
if player then
|
|
-- defeat entity attached bypass mechanism
|
|
player:set_detach()
|
|
minetest.kick_player(key, msg)
|
|
end
|
|
else
|
|
failed[key].n = val.n -- update
|
|
end
|
|
end
|
|
end
|
|
end
|
|
minetest.after(delay, save_failed)
|
|
end
|
|
minetest.after(delay, save_failed) -- initialise tmr
|
|
|
|
--[[
|
|
#######################
|
|
### Export/Import ###
|
|
#######################
|
|
]]
|
|
|
|
if importer then -- always true for first run
|
|
|
|
-- Load and deserialise xban2 file
|
|
-- @param filename string
|
|
-- @return table
|
|
local function load_xban(filename)
|
|
local f, e = ie.io.open(WP.."/"..filename, "rt")
|
|
if not f then
|
|
return false, "Unable to load xban2 database:" .. e
|
|
end
|
|
local content = f:read("*a")
|
|
f:close()
|
|
if not content then
|
|
return false, "Unable to load xban2 database: Read failed!"
|
|
end
|
|
local t = minetest.deserialize(content)
|
|
if not t then
|
|
return false, "xban2 database: Deserialization failed!"
|
|
end
|
|
return t
|
|
end
|
|
|
|
-- Load ipban file
|
|
-- @return string
|
|
local function load_ipban()
|
|
local f, e = ie.io.open(WP.."/ipban.txt")
|
|
if not f then
|
|
return false, "Unable to open 'ipban.txt': "..e
|
|
end
|
|
local content = f:read("*a")
|
|
f:close()
|
|
return content
|
|
end
|
|
|
|
-- Write sql file
|
|
-- @param string containing fle contents
|
|
-- @return nil
|
|
local function save_sql(txt)
|
|
local file = ie.io.open(WP.."/xban.sql", "a")
|
|
if file and txt then
|
|
file:write(txt)
|
|
file:close()
|
|
end
|
|
end
|
|
|
|
-- Delete sql file
|
|
-- @return nil
|
|
local function del_sql()
|
|
ie.os.remove(WP.."/xban.sql")
|
|
end
|
|
|
|
-- Create SQL string
|
|
-- @param id integer
|
|
-- @param entry keypair table
|
|
-- @return formatted string
|
|
local function sql_string(id, entry)
|
|
local names = {}
|
|
local ip = {}
|
|
local last_seen = entry.last_seen or 0
|
|
local last_pos = entry.last_pos or ""
|
|
|
|
-- names field includes both IP and names data, sort into 2 tables
|
|
for k, v in pairs(entry.names) do
|
|
if is_ip(k) then
|
|
table.insert(ip, k)
|
|
else
|
|
table.insert(names, k)
|
|
end
|
|
end
|
|
|
|
local q = ""
|
|
|
|
for i, v in ipairs(names) do
|
|
q = q..("INSERT INTO tmp_a VALUES (%i,'%s',%i,%i, 0);\n"
|
|
):format(id, v, last_seen, last_seen)
|
|
end
|
|
for i, v in ipairs(ip) do
|
|
-- address fields: id,ip,created,last_login,login_count,violation
|
|
q = q..("INSERT INTO tmp_b VALUES (%i,'%s',%i,%i,1,0);\n"
|
|
):format(id, v, last_seen, last_seen)
|
|
end
|
|
|
|
if #entry.record > 0 then
|
|
|
|
local ts = os.time()
|
|
-- bans to archive
|
|
for i, v in ipairs(entry.record) do
|
|
|
|
local expires = v.expires or 0
|
|
local reason = string.gsub(v.reason, "'", "''")
|
|
|
|
reason = string.gsub(reason, "%:%)", "") -- remove colons
|
|
|
|
if last_pos.y then
|
|
last_pos = vector.round(last_pos)
|
|
last_pos = minetest.pos_to_string(last_pos)
|
|
end
|
|
|
|
if entry.reason and entry.reason == v.reason then
|
|
-- active ban
|
|
-- fields: id,name,source,created,reason,expires,last_pos
|
|
q = q..("INSERT INTO tmp_c VALUES (%i,'%s','%s',%i,'%s',%i,'%s');\n"
|
|
):format(id, names[1], v.source, v.time, reason, expires, last_pos)
|
|
else
|
|
-- expired ban
|
|
-- fields: id,name,source,created,reason,expires,u_source,u_reason,
|
|
-- u_date,last_pos
|
|
q = q..("INSERT INTO tmp_d VALUES (%i,'%s','%s',%i,'%s',%i,'%s','%s',%i,'%s');\n"
|
|
):format(id, names[1], v.source, v.time, reason, expires, 'sban',
|
|
'expired prior to import', ts, last_pos)
|
|
end
|
|
end
|
|
end
|
|
|
|
return q
|
|
end
|
|
|
|
-- Import xban2 file active ban records
|
|
-- @param file_name string
|
|
-- @return nil
|
|
local function import_xban(file_name)
|
|
|
|
local t, err = load_xban(file_name)
|
|
|
|
if not t then -- exit with error message
|
|
return false, err
|
|
end
|
|
|
|
local id = ID
|
|
local bl = {}
|
|
local tl = {}
|
|
|
|
minetest.log("action", "processing "..#t.." records")
|
|
|
|
for i, v in ipairs(t) do
|
|
if v.banned == true then
|
|
bl[#bl+1] = v
|
|
t[i] = nil
|
|
end
|
|
end
|
|
|
|
minetest.log("action", "found "..#bl.." active ban records")
|
|
|
|
tl[#tl+1] = "PRAGMA foreign_keys = OFF;\n"
|
|
tl[#tl+1] = tmp_db
|
|
tl[#tl+1] = "BEGIN TRANSACTION;"
|
|
|
|
for i = #bl, 1, -1 do
|
|
if bl[i] then
|
|
id = id + 1
|
|
tl[#tl+1] = sql_string(id, bl[i])
|
|
bl[i] = nil -- shrink
|
|
end
|
|
end
|
|
|
|
tl[#tl+1] = tmp_final
|
|
-- run the prepared statement
|
|
db_exec(table.concat(tl, "\n"))
|
|
ID = id -- update global
|
|
return true
|
|
end
|
|
|
|
-- Import ipban file records
|
|
-- @param file_name string
|
|
-- @return nil
|
|
local function import_ipban(file_name)
|
|
local contents = load_ipban()
|
|
if not contents then
|
|
return false
|
|
end
|
|
local data = string.split(contents, "\n")
|
|
for i, v in ipairs(data) do
|
|
-- each line consists of an ip, separator and name
|
|
local ip, name = v:match("([^|]+)%|(.+)")
|
|
if ip and name then
|
|
-- check for an existing entry by name
|
|
local id = get_id(name)
|
|
if not id then
|
|
id = create_player(name, ip)
|
|
end
|
|
-- check for existing ban
|
|
if not bans[id] then
|
|
-- create ban entry - name,source,reason,expires
|
|
create_ban(name, 'sban', 'imported from ipban.txt', '')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Export xban2 file to SQL file
|
|
-- @param filename string
|
|
-- @return nil
|
|
local function export_sql(filename)
|
|
-- load the db, iterate in reverse order and remove each
|
|
-- record to balance the memory use otherwise large files
|
|
-- cause lua OOM error
|
|
local dbi, err = load_xban(filename)
|
|
local id = ID
|
|
if err then
|
|
minetest.log("warning", err)
|
|
return
|
|
end
|
|
-- reverse the contents
|
|
for i = 1, math.floor(#dbi / 2) do
|
|
local tmp = dbi[i]
|
|
dbi[i] = dbi[#dbi - i + 1]
|
|
dbi[#dbi - i + 1] = tmp
|
|
end
|
|
|
|
save_sql("PRAGMA foreign_keys = OFF;\n\n")
|
|
save_sql(createDb)
|
|
save_sql(tmp_db)
|
|
save_sql("BEGIN TRANSACTION;\n\n")
|
|
-- process records
|
|
for i = #dbi, 1, - 1 do
|
|
-- contains data?
|
|
if dbi[i] then
|
|
id = id + 1
|
|
local str = sql_string(id, dbi[i]) -- sql statement
|
|
save_sql(str)
|
|
dbi[i] = nil -- shrink
|
|
end
|
|
end
|
|
-- add sql inserts to transfer the data, clean up and finalise
|
|
save_sql(tmp_final)
|
|
end
|
|
|
|
-- Export db bans to xban2 file format
|
|
-- @return nil
|
|
local function export_to_xban()
|
|
local xport = {}
|
|
local DEF_DB_FILENAME = minetest.get_worldpath().."/xban.db"
|
|
local DB_FILENAME = minetest.setting_get("xban.db_filename")
|
|
|
|
if (not DB_FILENAME) or (DB_FILENAME == "") then
|
|
DB_FILENAME = DEF_DB_FILENAME
|
|
end
|
|
|
|
-- initialise table of banned id's
|
|
for k,v in pairs(bans) do
|
|
local id = v.id
|
|
xport[id] = {
|
|
banned = true,
|
|
names = {}
|
|
}
|
|
local t = {}
|
|
local q = ([[SELECT * FROM name
|
|
WHERE id = %i]]):format(id)
|
|
for row in db:nrows(q) do
|
|
xport[id].names[row.name] = true
|
|
end
|
|
q = ([[SELECT * FROM address
|
|
WHERE id = %i]]):format(id)
|
|
for row in db:nrows(q) do
|
|
xport[id].names[row.ip] = true
|
|
end
|
|
q = ([[SELECT * FROM expired WHERE id = %i;]]):format(id)
|
|
for row in db:nrows(q) do
|
|
t[#t+1] = {
|
|
time = row.created,
|
|
source = row.source,
|
|
reason = row.reason
|
|
}
|
|
end
|
|
t[#t+1] = {
|
|
time = bans[id].created,
|
|
source = bans[id].source,
|
|
reason = bans[id].reason
|
|
}
|
|
xport[id].record = t
|
|
xport[id].last_seen = bans[id].last_login
|
|
xport[id].last_pos = bans[id].last_pos or ""
|
|
end
|
|
|
|
local function repr(x)
|
|
if type(x) == "string" then
|
|
return ("%q"):format(x)
|
|
else
|
|
return tostring(x)
|
|
end
|
|
end
|
|
|
|
local function my_serialize_2(t, level)
|
|
level = level or 0
|
|
local lines = { }
|
|
local indent = ("\t"):rep(level)
|
|
for k, v in pairs(t) do
|
|
local typ = type(v)
|
|
if typ == "table" then
|
|
table.insert(lines,
|
|
indent..("[%s] = {\n"):format(repr(k))
|
|
..my_serialize_2(v, level + 1).."\n"
|
|
..indent.."},")
|
|
else
|
|
table.insert(lines,
|
|
indent..("[%s] = %s,"):format(repr(k), repr(v)))
|
|
end
|
|
end
|
|
return table.concat(lines, "\n")
|
|
end
|
|
|
|
local function this_serialize(t)
|
|
return "return {\n"..my_serialize_2(t, 1).."\n}"
|
|
end
|
|
|
|
local f, e = io.open(DB_FILENAME, "wt")
|
|
xport.timestamp = os.time()
|
|
if f then
|
|
local ok, err = f:write(this_serialize(xport))
|
|
if not ok then
|
|
minetest.log("error", "Unable to save database: %s", err)
|
|
end
|
|
else
|
|
minetest.log("error", "Unable to save database: %s", e)
|
|
end
|
|
if f then f:close() end
|
|
end
|
|
|
|
-- Register export to SQL file command
|
|
minetest.register_chatcommand("ban_dbe", {
|
|
description = "export xban2 db to sql format",
|
|
params = "<filename>",
|
|
privs = {server = true},
|
|
func = function(name, params)
|
|
local filename = params:match("%S+")
|
|
if not filename then
|
|
return false, "Use: /ban_dbe <filename>"
|
|
end
|
|
del_sql()
|
|
export_sql(filename)
|
|
return true, filename .. " dumped to xban.sql"
|
|
end
|
|
})
|
|
|
|
-- Register export to xban2 file format
|
|
minetest.register_chatcommand("ban_dbx", {
|
|
description = "export db to xban2 format",
|
|
privs = {server = true},
|
|
func = function(name)
|
|
export_to_xban()
|
|
return true, "dumped db to xban2 file!"
|
|
end
|
|
})
|
|
|
|
-- Register ban import command
|
|
minetest.register_chatcommand("ban_dbi", {
|
|
description = "Import bans",
|
|
params = "<filename>",
|
|
privs = {server = true},
|
|
func = function(name, params)
|
|
local filename = params:match("%S+")
|
|
if not filename then
|
|
return false, "Use: /ban_dbi <filename>"
|
|
end
|
|
local msg
|
|
if filename == "ipban.txt" then
|
|
import_ipban(name)
|
|
msg = "ipban.txt imported!"
|
|
else
|
|
local res, err = import_xban(filename)
|
|
msg = err
|
|
if res then
|
|
msg = filename.." bans imported!"
|
|
end
|
|
end
|
|
return true, msg
|
|
end
|
|
})
|
|
end
|
|
|
|
--[[
|
|
##############
|
|
### Misc ###
|
|
##############
|
|
]]
|
|
|
|
-- initialise config
|
|
local current_version = get_setting("db_version")
|
|
if not current_version then -- first run
|
|
init_setting('db_version', db_version)
|
|
init_setting('mod_version', mod_version)
|
|
elseif not current_version == db_version then
|
|
error("You must update sban database to "..db_version..
|
|
"\nUse sqlite3 to import /tools/sban_update.sql")
|
|
end
|
|
|
|
-- initialise caches
|
|
WL = get_whitelist()
|
|
bans = get_active_bans()
|
|
ID = last_id() or 0
|
|
owner_id = get_id(owner)
|
|
|
|
-- Manage expired bans
|
|
-- @return nil
|
|
local function process_expired_bans()
|
|
local ts = os.time()
|
|
local tq = {}
|
|
for id_key,row in pairs(bans) do
|
|
if type(row.expires) == "number" and row.expires ~= 0 then
|
|
-- temp ban
|
|
if ts > row.expires then
|
|
row.last_pos = row.last_pos or "" -- can't be nil!
|
|
-- add sql statements
|
|
tq[#tq+1] = ([[
|
|
INSERT INTO expired VALUES (%i,'%s','%s',%i,'%s',%i,'sban','tempban expired',%i,'%s');
|
|
DELETE FROM active WHERE id = %i;
|
|
]]):format(row.id, row.name, row.source, row.created, escape_string(row.reason),
|
|
row.expires, ts, row.last_pos, row.id)
|
|
end
|
|
end
|
|
end
|
|
if #tq > 0 then
|
|
-- finalise & execute
|
|
tq[#tq+1] = "VACUUM;"
|
|
db_exec(table.concat(tq, "\n"))
|
|
end
|
|
end
|
|
process_expired_bans() -- trigger on mod load
|
|
|
|
local function data_integrity_check()
|
|
local q
|
|
minetest.log("action", "[sban] Data integrity check...")
|
|
q = [[SELECT
|
|
active.id,
|
|
active.name,
|
|
active.source,
|
|
active.created,
|
|
active.reason
|
|
FROM active
|
|
LEFT JOIN name ON name.id = active.id
|
|
WHERE name.id IS NULL;]]
|
|
for row in db:nrows(q) do
|
|
minetest.log("warning", ([[[sban] id: %i %s %s %s %s is orphaned!]]
|
|
):format(row.id, row.name, row.source, hrdf(row.created), row.reason))
|
|
end
|
|
q = [[BEGIN TRANSACTION;
|
|
UPDATE active SET expires = 0 WHERE expires = '';
|
|
UPDATE expired SET expires = 0 WHERE expires = '';
|
|
COMMIT;]]
|
|
db_exec(q)
|
|
end
|
|
data_integrity_check() -- check for orphaned ban records!
|
|
|
|
-- fix irc mod with an override if present
|
|
if minetest.get_modpath('irc') ~= nil then
|
|
irc.reply = function(message) -- luacheck: ignore
|
|
if not irc.last_from then -- luacheck: ignore
|
|
return
|
|
end
|
|
message = message:gsub("[\r\n%z]", " \\n ")
|
|
local helper = string.split(message, "\\n")
|
|
for i,v in ipairs(helper) do
|
|
irc.say(irc.last_from, minetest.strip_colors(v)) -- luacheck: ignore
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[
|
|
###########
|
|
## GUI ##
|
|
###########
|
|
]]
|
|
|
|
-- Fetch and format ban info
|
|
-- @param entry keypair table
|
|
-- @return formatted string
|
|
local function create_info(entry)
|
|
-- returns an info string, line wrapped based on the ban record
|
|
if not entry then
|
|
return "something went wrong!\n Please reselect the entry."
|
|
end
|
|
local str = "Banned by: "..entry.source.."\n"
|
|
.."When: "..hrdf(entry.created).."\n"
|
|
if type(entry.expires) == "number" and entry.expires > 0 then
|
|
str = str.."Expires: "..hrdf(entry.expires).."\n"
|
|
else
|
|
str = str.."Expires: never, permanent ban!".."\n"
|
|
end
|
|
str = str .."Reason: " -- 8 chars
|
|
-- Word wrap 40 chars per line to fit in textbox
|
|
-- as its contents are allowed to be variable length
|
|
local words = entry.reason:split(" ") -- split reason into word array
|
|
local l,ctr = 40,8 -- initialise line length and start
|
|
for _,word in ipairs(words) do
|
|
local wl = word:len() -- chars in current word
|
|
if ctr + wl < l then -- does it fit on line?
|
|
str = str..word.." " -- add to line
|
|
ctr = ctr + (wl + 1) -- update pos
|
|
else
|
|
str = str.."\n"..word.." " -- add newline before word
|
|
ctr = wl + 1 -- update pos
|
|
end
|
|
end
|
|
return str
|
|
end
|
|
|
|
-- Fetch formstate, initialising if reqd
|
|
-- @param name string
|
|
-- @return keypair state table
|
|
local function get_state(name)
|
|
local s = formstate[name]
|
|
if not s then
|
|
s = {
|
|
list = {},
|
|
hlist = {},
|
|
index = -1,
|
|
info = "Select an entry from the list\n or use search",
|
|
banned = false,
|
|
ban = nil,
|
|
multi = false,
|
|
page = 1,
|
|
flag = false
|
|
}
|
|
formstate[name] = s
|
|
end
|
|
return s
|
|
end
|
|
|
|
-- Update state table
|
|
-- @param name string
|
|
-- @param selected string
|
|
-- @return nil
|
|
local function update_state(name, selected)
|
|
-- updates state used by formspec
|
|
local fs = get_state(name)
|
|
local id = get_id(selected)
|
|
local status = false
|
|
|
|
fs.ban = player_ban_expired(id)
|
|
local cur = bans[id]
|
|
if cur then
|
|
table.insert(fs.ban, cur)
|
|
status = true
|
|
end
|
|
|
|
local info = ("Banned: %s\n"):format(status)
|
|
.. "Ban records: "..#fs.ban.."\n"
|
|
|
|
fs.banned = cur
|
|
fs.multi = false
|
|
|
|
if #fs.ban == 0 then
|
|
info = info.."Player has no ban records!"
|
|
else
|
|
if not fs.flag then
|
|
fs.page = #fs.ban
|
|
fs.flag = true
|
|
end
|
|
if fs.page > #fs.ban then fs.page = #fs.ban end
|
|
info = info..create_info(fs.ban[fs.page])
|
|
end
|
|
|
|
fs.info = info
|
|
if #fs.ban > 1 then
|
|
fs.multi = true
|
|
end
|
|
end
|
|
|
|
-- Fetch user formspec
|
|
-- @param name string
|
|
-- @return formspec string
|
|
local function getformspec(name)
|
|
|
|
local fs = formstate[name]
|
|
local f
|
|
local list = fs.list
|
|
local bgimg = ""
|
|
if default and default.gui_bg_img then
|
|
bgimg = default.gui_bg_img
|
|
end
|
|
|
|
f = {}
|
|
f[#f+1] = "size[8,6.6]"
|
|
f[#f+1] = bgimg
|
|
f[#f+1] = "field[0.3,0.4;4.5,0.5;search;;]"
|
|
f[#f+1] = "field_close_on_enter[search;false]"
|
|
f[#f+1] = "button[4.5,0.1;1.5,0.5;find;Find]"
|
|
if #fs.list > 0 then
|
|
f[#f+1] = "textlist[0,0.9;2.4,3.6;plist;"
|
|
local tmp = {}
|
|
for i,v in ipairs(list) do
|
|
tmp[#tmp+1] = v
|
|
end
|
|
f[#f+1] = table.concat(tmp, ",")
|
|
f[#f+1] = ";"
|
|
f[#f+1] = fs.index
|
|
f[#f+1] = "]"
|
|
end
|
|
f[#f+1] = "field[0.3,6.5;4.5,0.5;reason;Reason:;]"
|
|
f[#f+1] = "field_close_on_enter[reason;false]"
|
|
|
|
if fs.multi == true then
|
|
f[#f+1] = "image_button[6,0.1;0.5,0.5;ui_left_icon.png;left;]"
|
|
f[#f+1] = "image_button[7,0.1;0.5,0.5;ui_right_icon.png;right;]"
|
|
if fs.page > 9 then
|
|
f[#f+1] = "label[6.50,0.09;"
|
|
f[#f+1] = fs.page
|
|
f[#f+1] = "]"
|
|
else
|
|
f[#f+1] = "label[6.55,0.09;"
|
|
f[#f+1] = fs.page
|
|
f[#f+1] = "]"
|
|
end
|
|
end
|
|
|
|
f[#f+1] = "label[2.6,0.9;"
|
|
f[#f+1] = fs.info
|
|
f[#f+1] = "]"
|
|
|
|
if fs.banned then
|
|
f[#f+1] = "button[4.5,6.2;1.5,0.5;unban;Unban]"
|
|
else
|
|
f[#f+1] = "field[0.3,5.5;2.6,0.3;duration;Duration:;"
|
|
f[#f+1] = def_duration
|
|
f[#f+1] = "]"
|
|
f[#f+1] = "field_close_on_enter[duration;false]"
|
|
f[#f+1] = "button[4.5,6.2;1.5,0.5;ban;Ban]"
|
|
f[#f+1] = "button[6,6.2;2,0.5;tban;Temp Ban]"
|
|
end
|
|
|
|
return table.concat(f)
|
|
end
|
|
|
|
-- Register form submission callbacks
|
|
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
|
|
if formname ~= FORMNAME then return end
|
|
|
|
local name = player:get_player_name()
|
|
local privs = minetest.get_player_privs(name)
|
|
local fs = get_state(name)
|
|
|
|
if not privs.ban then
|
|
-- legitimate clients cannot access this form without privs
|
|
-- so ban them for having a dodgy client
|
|
minetest.log(
|
|
"warning", "[sban] Received fields from unauthorized user: "..name)
|
|
create_ban(name, 'sban', 'detected using an unauthorised client!')
|
|
return
|
|
end
|
|
|
|
if fields.find then
|
|
|
|
if fields.search:len() > 2 then
|
|
fs.list = get_names(ESC(fields.search))
|
|
else
|
|
fs.list = fs.hlist
|
|
end
|
|
local str = "No record found!"
|
|
if #fs.list > 0 then
|
|
str = "Select an entry to see the details..."
|
|
end
|
|
fs.info = str
|
|
fs.index = -1
|
|
fs.multi = false
|
|
minetest.show_formspec(name, FORMNAME, getformspec(name))
|
|
|
|
elseif fields.plist then
|
|
|
|
local t = minetest.explode_textlist_event(fields.plist)
|
|
|
|
if (t.type == "CHG") or (t.type == "DCL") then
|
|
|
|
fs.index = t.index
|
|
fs.flag = false -- reset
|
|
update_state(name, fs.list[t.index])
|
|
minetest.show_formspec(name, FORMNAME, getformspec(name))
|
|
end
|
|
|
|
elseif fields.left or fields.right then
|
|
|
|
if fields.left then
|
|
if fs.page > 1 then fs.page = fs.page - 1 end
|
|
else
|
|
if fs.page < #fs.ban then fs.page = fs.page + 1 end
|
|
end
|
|
update_state(name, fs.list[fs.index])
|
|
minetest.show_formspec(name, FORMNAME, getformspec(name))
|
|
|
|
elseif fields.ban or fields.unban or fields.tban then
|
|
|
|
local selected = fs.list[fs.index]
|
|
local id = get_id(selected)
|
|
|
|
if fields.reason ~= "" then
|
|
if fields.ban then
|
|
if selected == owner then
|
|
fs.info = "you do not have permission to do that!"
|
|
else
|
|
create_ban(selected, name, ESC(fields.reason), 0)
|
|
end
|
|
elseif fields.unban then
|
|
local res = update_ban(id, name, ESC(fields.reason), selected)
|
|
if res then
|
|
fs.ban = player_ban_expired(id)
|
|
end
|
|
elseif fields.tban then
|
|
if selected == owner then
|
|
fs.info = "you do not have permission to do that!"
|
|
else
|
|
local t = parse_time(ESC(fields.duration)) + os.time()
|
|
create_ban(selected, name, ESC(fields.reason), t)
|
|
end
|
|
end
|
|
fs.flag = false -- reset
|
|
update_state(name, selected)
|
|
else
|
|
fs.info = "You must supply a reason!"
|
|
end
|
|
minetest.show_formspec(name, FORMNAME, getformspec(name))
|
|
end
|
|
end)
|
|
|
|
--[[
|
|
################################
|
|
### Register Chat Commands ###
|
|
################################
|
|
]]
|
|
|
|
-- Register ban command
|
|
minetest.override_chatcommand("ban", {
|
|
description = "Bans a player from the server",
|
|
params = "<player> <reason>",
|
|
privs = { ban = true },
|
|
func = function(name, params)
|
|
local player_name, reason = params:match("(%S+)%s+(.+)")
|
|
|
|
if not (player_name and reason) then
|
|
-- check params are present
|
|
return false, "Usage: /ban <player> <reason>"
|
|
end
|
|
|
|
if player_name == owner then
|
|
-- protect owner
|
|
return false, "Insufficient privileges!"
|
|
end
|
|
|
|
local expires = 0
|
|
local id = get_id(player_name)
|
|
local res, msg
|
|
|
|
msg = ("Banned %s."):format(player_name)
|
|
|
|
if id then
|
|
-- check for existing ban
|
|
if bans[id] then
|
|
return true, ("%s is already banned!"):format(player_name)
|
|
end
|
|
-- limit ban?
|
|
if expiry then
|
|
expires = parse_time(expiry) + os.time()
|
|
end
|
|
-- Params: name, source, reason, expires
|
|
res = create_ban(player_name, name, reason, expires)
|
|
else
|
|
local privs = minetest.get_player_privs(name)
|
|
-- ban_admin only
|
|
if not privs.ban_admin then
|
|
return false, "Player "..player_name.." doesn't exist!"
|
|
end
|
|
-- create entry & ban
|
|
add_name(inc_id(), player_name)
|
|
res = create_ban(player_name, name, reason, expires)
|
|
end
|
|
if not res then msg = ("Failed to ban %s!"):format(player_name) end
|
|
return true, msg
|
|
end
|
|
})
|
|
|
|
-- Register unban command
|
|
minetest.override_chatcommand("unban", {
|
|
description = "Unban a player or ip banned with sban",
|
|
params = "<player_or_ip> <reason>",
|
|
privs = { ban = true },
|
|
func = function(name, params)
|
|
local player_name, reason = params:match("(%S+)%s+(.+)")
|
|
if not (player_name and reason) then
|
|
return false, "Usage: /unban <player_or_ip> <reason>"
|
|
end
|
|
-- look for records by id
|
|
local id = get_id(player_name)
|
|
if id then
|
|
if not bans[id] then
|
|
return true, ("No active ban record for "..player_name)
|
|
end
|
|
update_ban(id, name, reason, player_name)
|
|
return true, ("Unbanned %s."):format(player_name)
|
|
end
|
|
end,
|
|
})
|
|
|
|
|
|
-- Register temp ban command
|
|
minetest.register_chatcommand("tempban", {
|
|
description = "Ban a player temporarily with sban",
|
|
params = "<player> <time> <reason>",
|
|
privs = { ban = true },
|
|
func = function(name, params)
|
|
local player_name, time, reason = params:match("(%S+)%s+(%S+)%s+(.+)")
|
|
|
|
if not (player_name and time and reason) then
|
|
-- correct params?
|
|
return false, "Usage: /tempban <player> <time> <reason>"
|
|
end
|
|
|
|
if player_name == owner then
|
|
-- protect owner account
|
|
return false, "Insufficient privileges!"
|
|
end
|
|
|
|
time = parse_time(time)
|
|
if time < 60 then
|
|
return false, "You must ban for at least 60 seconds."
|
|
end
|
|
local expires = os.time() + time
|
|
|
|
-- is player already banned?
|
|
local id = get_id(player_name)
|
|
local res, msg
|
|
|
|
msg = ("Banned %s until %s."):format(player_name, os.date("%c", expires))
|
|
if id then
|
|
if bans[id] then
|
|
return true, ("%s is already banned!"):format(player_name)
|
|
end
|
|
res = create_ban(player_name, name, reason, expires)
|
|
else
|
|
local privs = minetest.get_player_privs(name)
|
|
-- assert normal behaviour without admin priv
|
|
if not privs.ban_admin then
|
|
return false, "Player doesn't exist!"
|
|
end
|
|
-- create entry before ban
|
|
add_name(inc_id(), player_name)
|
|
res = create_ban(player_name, name, reason, expires)
|
|
end
|
|
if not res then msg = ("Failed to ban %s!"):format(player_name) end
|
|
return true, msg
|
|
end,
|
|
})
|
|
|
|
-- Register info command
|
|
minetest.register_chatcommand("ban_record", {
|
|
description = "Display player sban records",
|
|
params = "<player_or_ip>",
|
|
privs = { ban = true },
|
|
func = function(name, params)
|
|
local playername = params:match("%S+")
|
|
if not playername or playername:find("*") then
|
|
return false, "usage: /ban_record <player_name>"
|
|
end
|
|
-- get target and source privs
|
|
local id = get_id(playername)
|
|
if not id then
|
|
return false, "Unknown player!"
|
|
end
|
|
local target = name_records(id)
|
|
local source = minetest.get_player_privs(name)
|
|
local chk = false
|
|
for i, v in ipairs(target) do
|
|
local privs = minetest.get_player_privs(v.name)
|
|
if privs.server then chk = true break end
|
|
end
|
|
-- if source doesn't have sufficient privs deny & inform
|
|
if not source.server and chk then
|
|
return false, "Insufficient privileges to access that information"
|
|
end
|
|
return true, display_record(name, playername)
|
|
end
|
|
})
|
|
|
|
-- Register ban deletion command
|
|
minetest.register_chatcommand("ban_del", {
|
|
description = "Deletes a player's sban records",
|
|
params = "player",
|
|
privs = {server = true},
|
|
func = function(name, params)
|
|
local player_name = params:match("%S+")
|
|
if not player_name then
|
|
return false, "Usage: /ban_del <player>"
|
|
end
|
|
local id = get_id(player_name)
|
|
if not id then
|
|
return false, player_name.." doesn't exist!"
|
|
end
|
|
del_ban(id)
|
|
minetest.log("action",
|
|
"ban records for "..player_name.." deleted by "..name)
|
|
return true, player_name.." ban records deleted!"
|
|
end
|
|
})
|
|
|
|
-- Register whitelist command
|
|
minetest.register_chatcommand("ban_wl", {
|
|
description = "Manages the whitelist",
|
|
params = "(add|del|list) <name_or_ip>",
|
|
privs = {server = true},
|
|
func = function(name, params)
|
|
local helper = ("Usage: /ban_wl (add|del) "
|
|
.."<name_or_ip> \nor /ban_wl list")
|
|
local param = {}
|
|
local i = 1
|
|
for word in params:gmatch("%S+") do
|
|
param[i] = word
|
|
i = i + 1
|
|
end
|
|
if #param < 1 then
|
|
return false, helper
|
|
end
|
|
if param[1] == "list" then
|
|
local str = ""
|
|
for k, v in pairs(WL) do
|
|
str = str..k.."\n"
|
|
end
|
|
if str ~= "" then
|
|
return true, str
|
|
end
|
|
return true, "Whitelist empty!"
|
|
end
|
|
if param[2] then
|
|
if param[1] == "add" then
|
|
if not WL[param[2]] then
|
|
add_whitelist_record(name, param[2])
|
|
WL[param[2]] = true
|
|
minetest.log("action",
|
|
("%s added %s to whitelist"):format(name, param[2]))
|
|
return true, param[2].." added to whitelist!"
|
|
else
|
|
return false, param[2].." is already whitelisted!"
|
|
end
|
|
elseif param[1] == "del" then
|
|
if WL[param[2]] then
|
|
del_whitelist_record(param[2])
|
|
WL[param[2]] = nil
|
|
minetest.log("action", ("%s removed %s from whitelist"
|
|
):format(name, param[2]))
|
|
return true, param[2].." removed from whitelist!"
|
|
else
|
|
return false, param[2].." isn't on the whitelist"
|
|
end
|
|
end
|
|
end
|
|
return false, helper
|
|
end
|
|
})
|
|
|
|
-- Register GUI command
|
|
minetest.register_chatcommand("bang", {
|
|
description = "Launch sban gui",
|
|
privs = {ban = true},
|
|
func = function(name)
|
|
formstate[name] = nil
|
|
local fs = get_state(name)
|
|
fs.list = hotlist
|
|
for i,v in ipairs(fs.list) do
|
|
fs.hlist[i] = v
|
|
end
|
|
minetest.show_formspec(name, FORMNAME, getformspec(name))
|
|
end
|
|
})
|
|
|
|
-- Register kick command (reqd for 5.0 ?)
|
|
minetest.override_chatcommand("kick", {
|
|
params = "<name> [reason]",
|
|
description = "Kick a player",
|
|
privs = {kick=true},
|
|
func = function(name, param)
|
|
local tokick, reason = param:match("([^ ]+) (.+)")
|
|
tokick = tokick or param
|
|
local player = minetest.get_player_by_name(tokick)
|
|
if not player then
|
|
return false, "Player " .. tokick .. " not in game!"
|
|
end
|
|
if not minetest.kick_player(tokick, reason) then
|
|
player:set_detach()
|
|
if not minetest.kick_player(tokick, reason) then
|
|
return false, "Failed to kick player " .. tokick ..
|
|
" after detaching!"
|
|
end
|
|
end
|
|
local log_reason = ""
|
|
if reason then
|
|
log_reason = " with reason \"" .. reason .. "\""
|
|
end
|
|
minetest.log("action", name .. " kicks " .. tokick .. log_reason)
|
|
return true, "Kicked " .. tokick
|
|
end,
|
|
})
|
|
|
|
-- Register whois command
|
|
minetest.register_chatcommand("/whois", {
|
|
params = "<player> [v]",
|
|
description = "Returns player information, use v for verbose output.",
|
|
privs = {ban_admin = true},
|
|
func = function(name, param)
|
|
local list = {}
|
|
for word in param:gmatch("%S+") do
|
|
list[#list+1] = word
|
|
end
|
|
if #list < 1 then
|
|
return false, "usage: /whois <player> [v]"
|
|
end
|
|
local pname = list[1]
|
|
local id = get_id(pname)
|
|
if not id then
|
|
return false, "The player \"" .. pname .. "\" did not join yet."
|
|
end
|
|
local names = name_records(id)
|
|
local ips = address_records(id)
|
|
local msg = "\n" .. minetest.colorize("#FFC000", "Names: ")
|
|
local n, a = {}, {}
|
|
for i, v in ipairs(names) do
|
|
n[#n+1] = v.name
|
|
end
|
|
for i, v in ipairs(ips) do
|
|
a[#a+1] = v.ip
|
|
end
|
|
msg = msg .. table.concat(n, ", ")
|
|
if #list > 1 and list[2] == "v" then
|
|
msg = msg .. minetest.colorize("#FFC000", "IP Addresses: ")
|
|
msg = msg .. "\n" .. table.concat(a, ", ")
|
|
else
|
|
msg = msg .. "\n" .. minetest.colorize("#FFC000", "Last IP Address: ")
|
|
msg = msg .. a[1]
|
|
end
|
|
return true, minetest.colorize("#FFC000", "Info for: ") .. pname .. msg
|
|
end,
|
|
})
|
|
|
|
--[[
|
|
#######################
|
|
### API Functions ###
|
|
#######################
|
|
]]
|
|
|
|
if api then
|
|
-- Ban function
|
|
-- @param name string
|
|
-- @param source string
|
|
-- @param reason string
|
|
-- @param expires alphanumerical duration string or integer
|
|
-- @return bool
|
|
-- @return msg string
|
|
sban.ban = function(name, source, reason, expires)
|
|
-- check params are valid
|
|
assert(type(name) == 'string')
|
|
assert(type(source) == 'string')
|
|
assert(type(reason) == 'string')
|
|
if expires and type(expires) == 'string' then
|
|
expires = parse_time(expires)
|
|
elseif expires and type(expires) == "integer" then
|
|
local ts = os.time()
|
|
if expires < ts then
|
|
expires = ts + expires
|
|
end
|
|
end
|
|
if name == owner then
|
|
return false, 'insufficient privileges!'
|
|
end
|
|
local id = get_id(name)
|
|
local res, msg
|
|
msg = ("Banned %s."):format(name)
|
|
if not id then
|
|
return false, ("No records exist for %s"):format(name)
|
|
elseif bans[id] then
|
|
-- only one active ban per id is reqd!
|
|
return false, ("An active ban already exist for %s"):format(name)
|
|
end
|
|
-- ban player
|
|
res = create_ban(name, source, reason, expires)
|
|
if not res then msg = ("Failed to ban %s."):format(name) end
|
|
return res, msg
|
|
end
|
|
|
|
-- Unban function
|
|
-- @param name string
|
|
-- @param source name string
|
|
-- @param reason string
|
|
-- @return bool and msg string or nil
|
|
sban.unban = function(name, source, reason)
|
|
-- check params are valid
|
|
assert(type(name) == 'string')
|
|
assert(type(source) == 'string')
|
|
assert(type(reason) == 'string')
|
|
-- look for records by id
|
|
local id = get_id(name)
|
|
if id then
|
|
if not bans[id] then
|
|
return false, ("No active ban record for "..name)
|
|
end
|
|
update_ban(id, name, reason, name)
|
|
return true, ("Unbanned %s"):format(name)
|
|
else
|
|
return false, ("No records exist for %s"):format(name)
|
|
end
|
|
end
|
|
|
|
-- Fetch ban status for a player name or ip address
|
|
-- @param name_or_ip string
|
|
-- @return bool
|
|
sban.ban_status = function(name_or_ip)
|
|
assert(type(name_or_ip) == 'string')
|
|
local id = get_id(name_or_ip)
|
|
return bans[id] ~= nil
|
|
end
|
|
|
|
-- Fetch ban status for a player name or ip address
|
|
-- @param name_or_ip string
|
|
-- @return keypair table record
|
|
sban.ban_record = function(name_or_ip)
|
|
assert(type(name_or_ip) == 'string')
|
|
local id = get_id(name_or_ip)
|
|
if id then
|
|
return bans[id]
|
|
end
|
|
end
|
|
|
|
-- Fetch active bans
|
|
-- @return keypair table of active bans
|
|
sban.list_active = function()
|
|
return bans
|
|
end
|
|
end
|
|
|
|
--[[
|
|
############################
|
|
### Register callbacks ###
|
|
############################
|
|
]]
|
|
|
|
-- Register callback for shutdown event
|
|
minetest.register_on_shutdown(function()
|
|
db:close()
|
|
end)
|
|
|
|
-- Register callback for prejoin event
|
|
minetest.register_on_prejoinplayer(function(name, ip)
|
|
|
|
-- Force player data to be cached if known
|
|
local id = get_id(name) or get_id(ip)
|
|
|
|
if not id then return end -- unknown player
|
|
|
|
-- whitelist bypass
|
|
if WL[name] or WL[ip] then
|
|
minetest.log("action", "[sban] " .. name .. " whitelist login")
|
|
return
|
|
end
|
|
|
|
if not dev and owner_id and owner_id == id then return end -- owner bypass
|
|
|
|
local data = bans[id]
|
|
|
|
if not data then
|
|
-- check names per id
|
|
if names_per_id then
|
|
-- names per id
|
|
local names = name_records(id)
|
|
-- allow existing
|
|
for _,v in ipairs(names) do
|
|
if v.name == name then return end
|
|
end
|
|
-- check player isn't exceeding account limit
|
|
if #names >= names_per_id then
|
|
-- create string list
|
|
local msg = ""
|
|
for _,v in ipairs(names) do
|
|
msg = msg..v.name..", "
|
|
end
|
|
msg = msg:sub(1, msg:len() - 2) -- trim trailing ','
|
|
return ("\nYou exceeded the limit of accounts ("..
|
|
names_per_id..").\nYou already have the following accounts:\n"
|
|
..msg)
|
|
end
|
|
end
|
|
-- check ip's per id
|
|
if ip_limit then
|
|
local t = address_records(id)
|
|
for _,v in ipairs(t) do
|
|
if v.ip == ip then return end
|
|
end
|
|
if #t >= ip_limit then
|
|
return "\nYou exceeded the limit of ip addresses for an account!"
|
|
end
|
|
end
|
|
|
|
else
|
|
-- check for ban expiry
|
|
local date
|
|
|
|
if type(data.expires) == "number" and data.expires ~= 0 then
|
|
-- temp ban
|
|
if os.time() > data.expires then
|
|
-- clear temp ban
|
|
update_ban(data.id, "sban", "ban expired", name)
|
|
return
|
|
end
|
|
date = hrdf(data.expires)
|
|
else
|
|
date = "the end of time"
|
|
end
|
|
return ("Banned: Expires: %s, Reason: %s"):format(date, data.reason)
|
|
end
|
|
|
|
end)
|
|
|
|
-- Register callback for join event
|
|
minetest.register_on_joinplayer(function(player)
|
|
|
|
local name = player:get_player_name()
|
|
local id, ip
|
|
|
|
ip = minetest.get_player_ip(name)
|
|
|
|
if not ip then
|
|
minetest.log("error", [[[sban] minetest.get_player_ip(name) in
|
|
register_on_joinplayer callback returned nil!]])
|
|
return
|
|
end
|
|
|
|
manage_hotlist(name)
|
|
trim_cache()
|
|
|
|
id = get_id(name)
|
|
if not id then
|
|
-- unknown name
|
|
id = get_id(ip) -- ip search
|
|
if not id then
|
|
-- no player records found, create one
|
|
id = create_player(name, ip)
|
|
if type(id) == "number" and id > 0 then
|
|
-- owner id init check
|
|
if not owner_id and name == owner then
|
|
owner_id = id -- initialise
|
|
end
|
|
end
|
|
else
|
|
-- new name record an id
|
|
add_name(id, name)
|
|
end
|
|
else
|
|
-- check ip record
|
|
local target_id = get_id(ip)
|
|
if not target_id then
|
|
-- unknown ip
|
|
add_ip(id, ip) -- new address record
|
|
elseif target_id ~= id then
|
|
-- ip registered to another id!
|
|
manage_idv(id, target_id, ip)
|
|
update_idv_status(ip)
|
|
else
|
|
update_address(id, ip)
|
|
end
|
|
-- update name record timestamp
|
|
update_login(id, name)
|
|
end
|
|
end)
|