sban/init.lua
sofar b5885a6f2e Gui: all luachecked and tested. (#11)
* Obvious and simple luacheck motivated fixes.

* Use minetest.log().

* Avoid single execution loops.

contentious, but correct. Needs testing for sure.

* add a GUI

adds the convenience of a GUI to administer ban/tempban/unban and check player ban records

* rearange code structure and add checks, tidy up

* change chat command

* add textures

* Update README.md

* fix missing name in unban logging

* Update README.md

* fixes issue #1

* remove background for screen

* fix error with name in prejoin

* fix crash in prejoin & missing declaration in ban

* dont display list if it's empty

* add missing assingment for prev commit

* add info if no record found when using the gui

* add missing format to active_ban_record

* add conf setting for tempban duration

* Update README.md

* check IP on prejoin

* add a check for data in prejoin

* get ban data with ip if name fails in prejoin

* simplify ban search for prejoin event

* Properly fix single-query sql sequences.

* Luacheck fixes.

* Since `default` is optional (and should be), fix crash if it is missing.

* Fix "nil unbanned by..." display issue.
2018-01-06 20:25:42 +00:00

1624 lines
38 KiB
Lua

-- sban mod for minetest voxel game
-- designed and coded by shivajiva101@hotmail.com
local WP = minetest.get_worldpath()
local WL
local ie = minetest.request_insecure_environment()
local ESC = minetest.formspec_escape
local hotlist
if not ie then
error("insecure environment inaccessible"..
" - make sure this mod has been added to minetest.conf!")
end
-- requires library for db access
local _sql = ie.require("lsqlite3")
-- prevent other mods from using the global sqlite3 library
if sqlite3 then sqlite3 = nil end
minetest.register_privilege("ban_admin", "Bans administrator")
local db_version = "0.1"
local db = _sql.open(WP.."/sban.sqlite") -- connection
local expiry = minetest.setting_get("sban.ban_max")
local owner = minetest.setting_get("name")
local def_duration = minetest.setting_get("sban.fs_duration") or "1w"
local display_max = minetest.setting_get("sban.display_max") or 10
local t_units = {
s = 1, m = 60, h = 3600,
d = 86400, w = 604800, M = 2592000, y = 31104000,
D = 86400, W = 604800, Y = 31104000,
[""] = 1,
}
-- db:exec wrapper for error reporting
local function db_exec(stmt)
if db:exec(stmt) ~= _sql.OK then
minetest.log("info", "Sqlite ERROR: "..db:errmsg())
end
end
--[[
#########################
### Parse Functions ###
#########################
]]
-- convert value to seconds, copied from xban2 mod and modified
local function parse_time(t)
local s = 0
for n, u in t:gmatch("(%d+)([smhdwyDMY]?)") do
s = s + (tonumber(n) * (t_units[u] or 1))
end
return s
end
-- human readable date format - converts UTC
local function hrdf(t)
if type(t) == "number" then
return (t and os.date("%c", t))
end
end
--[[
##########################
### Database: Tables ###
##########################
]]
createDb = "CREATE TABLE IF NOT EXISTS bans (id INTEGER, "
.."name VARCHAR(50), source VARCHAR(50), created INTEGER, "
.."reason VARCHAR(300), expires INTEGER, u_source VARCHAR(50), "
.."u_reason VARCHAR(300), u_date INTEGER, active BOOLEAN, "
.."last_pos VARCHAR(50));\n"
.."CREATE TABLE IF NOT EXISTS playerdata (id INTEGER, "
.."name VARCHAR(50), ip VARCHAR(50), created INTEGER, "
.."last_login INTEGER);\nCREATE TABLE IF NOT EXISTS players ("
.."id INTEGER PRIMARY KEY AUTOINCREMENT, ban BOOLEAN);\n"
.."CREATE TABLE IF NOT EXISTS whitelist (name VARCHAR(50), "
.."source VARCHAR(50), created INTEGER);\n"
.."CREATE TABLE IF NOT EXISTS version (rev VARCHAR(20));\n"
db_exec(createDb)
--[[
###########################
### Database: Queries ###
###########################
]]
local function get_id(name_or_ip)
local q
if name_or_ip:find("%.") then
q = ([[
SELECT players.id
FROM players
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE playerdata.ip = '%s' LIMIT 1;]]
):format(name_or_ip)
else
q = ([[
SELECT players.id
FROM players
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE playerdata.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
local function next_id()
local q = [[SELECT seq FROM sqlite_sequence WHERE name= "players"]]
-- returns an integer for last id
local it, state = db:nrows(q)
local row = it(state)
if row then
return row.seq + 1 -- next id
end
end
local function qbc(id)
local q = ([[
SELECT ban
FROM players
WHERE id = '%i' LIMIT 1;
]]):format(id)
local result = false
for row in db:nrows(q) do
if row.ban == 'true' then result = true end
end
return result
end
local function active_ban_record(id)
local q = ([[
SELECT active
FROM bans
WHERE id = '%i' AND
active = 'true' LIMIT 1;
]]):format(id)
local it, state = db:nrows(q)
local row = it(state)
return row ~= nil
end
local function check_ban(id)
local q = ([[
SELECT players.id,
playerdata.ip,
bans.reason,
bans.expires
FROM players
INNER JOIN
bans ON players.id = bans.id
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE players.ban = 'true' AND
playerdata.id = '%i' AND
bans.active = 'true' LIMIT 1;
]]):format(id)
-- fill return table
local it, state = db:nrows(q)
local row = it(state)
return row
end
local function is_banned(name_or_ip)
local q
if name_or_ip:find("%.") then
q = ([[
SELECT players.id,
playerdata.ip,
bans.reason,
bans.expires
FROM players
INNER JOIN
bans ON players.id = bans.id
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE players.ban = 'true' AND
playerdata.ip = '%s' AND
bans.active = 'true' LIMIT 1;
]]):format(name_or_ip)
else
q = ([[
SELECT players.id,
playerdata.ip,
bans.reason,
bans.expires
FROM players
INNER JOIN
bans ON players.id = bans.id
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE players.ban = 'true' AND
playerdata.name = '%s' AND
bans.active = 'true' LIMIT 1;
]]):format(name_or_ip)
end
-- return record
local it, state = db:nrows(q)
local row = it(state)
return row
end
local function list_bans(id)
local r = {}
local q = ([[
SELECT
bans.id,
bans.name,
bans.reason,
bans.created,
bans.source,
bans.expires,
bans.u_source,
bans.u_reason,
bans.u_date,
bans.active,
bans.last_pos
FROM bans
WHERE bans.id = '%s'
]]):format(id)
-- fill return table
for row in db:nrows(q) do
r[#r + 1] = row
end
return r
end
local function find_records(name_or_ip)
local r,q = {}
if name_or_ip:find("%.") then
-- construct
q = ([[
SELECT players.id,
players.ban,
playerdata.name,
playerdata.ip,
playerdata.created,
playerdata.last_login
FROM players
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE playerdata.ip = '%s';
]]):format(name_or_ip)
else
q = ([[
SELECT players.id,
players.ban,
playerdata.name,
playerdata.ip,
playerdata.created,
playerdata.last_login
FROM players
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE playerdata.name = '%s';
]]):format(name_or_ip)
end
-- fill return table
for row in db:nrows(q) do
r[#r + 1] = row
end
return r
end
local function find_records_by_id(id)
local r = {}
local q = ([[
SELECT players.id,
players.ban,
playerdata.name,
playerdata.ip,
playerdata.created,
playerdata.last_login
FROM players
INNER JOIN
playerdata ON playerdata.id = players.id
WHERE playerdata.id = '%s'
]]):format(id)
for row in db:nrows(q) do
r[#r + 1] = row
end
return r
end
local function get_whitelist()
local r = {}
local query = "SELECT * FROM whitelist"
for row in db:nrows(query) do
r[row.name] = true
end
return r
end
local function get_version()
local query = "SELECT * FROM version"
local it, state = db:nrows(query)
local row = it(state)
if row then
return row.rev
end
end
local function display_record(name, p_name)
local id = get_id(p_name)
local r = find_records_by_id(id)
if #r == 0 then
minetest.chat_send_player(name, "No records for "..p_name)
return
end
local privs = minetest.get_player_privs(name)
-- records loaded, display
local idx = 1
if #r > display_max then
idx = #r - display_max
minetest.chat_send_player(name,
"Player records: "..#r.." (showing last "..display_max.." records)")
else
minetest.chat_send_player(name,
"Player records: "..#r)
end
if privs.ban_admin == true then
for i = idx, #r do
-- format utc values
local d1 = hrdf(r[i].created)
local d2 = hrdf(r[i].last_login)
minetest.chat_send_player(name,
("[%s] Name: %s IP: %s Created: %s Last login: %s"
):format(i, r[i].name, r[i].ip, d1, d2))
end
else
for i = idx, #r do
local d1 = hrdf(r[i].created)
local d2 = hrdf(r[i].last_login)
minetest.chat_send_player(name,
("[%s] Name: %s Created: %s Last login: %s"
):format(i, r[i].name, d1, d2))
end
end
local t = list_bans(id) or {}
if #t > 0 then
minetest.chat_send_player(name, "Ban records: "..#t)
local ban = t[#t].active
for i, e in ipairs(t) do
local d1 = hrdf(e.created)
local expires
if type(e.expires) == "number" then
expires = hrdf(e.expires)
else
expires = "never"
end
if type(e.u_date) == "number"
and e.u_date > 0 then
local d2 = hrdf(e.u_date)
minetest.chat_send_player(name,
("[%s] Name: %s Created: %s Banned by: %s Reason: %s Expires: %s"
):format(i, e.name, d1, e.source, e.reason, expires))
minetest.chat_send_player(name,
("[%s] Unbanned by: %s Reason: %s Time: %s"
):format(i, e.u_source, e.u_reason, d2))
else
minetest.chat_send_player(name,
("[%s] Name: %s Created: %s Banned by: %s Reason: %s Expires: %s"
):format(i, e.name, d1, e.source, e.reason, expires))
end
end
minetest.chat_send_player(name, "Banned: "..ban)
else
minetest.chat_send_player(name, "No Ban records!")
end
end
local function get_names(name)
local r,t,q = {},{}
q = "SELECT name FROM playerdata 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
--[[
###########################
### Database: Inserts ###
###########################
]]
local function create_entry(player_name, ip_address)
-- players table id is auto incremented
-- id,ban
db_exec[[
INSERT INTO players (ban)
VALUES ('false')
]]
-- retrieve id
local id = next_id() - 1
-- create timestamp
local ts = os.time()
-- id,name,ip,created,last_login
local stmt = ([[
INSERT INTO playerdata
VALUES (%s,'%s','%s',%s,%s)
]]):format(id, player_name, ip_address, ts, ts)
db_exec(stmt)
return id
end
local function add_player(id, player_name, ip_address)
local ts = os.time()
local stmt = ([[
INSERT INTO playerdata
VALUES (%s,'%s','%s',%s,%s)
]]):format(id, player_name, ip_address, ts, ts)
db_exec(stmt)
end
local function add_whitelist(source, name_or_ip)
local ts = os.time()
local stmt = ([[
INSERT INTO whitelist
VALUES ('%s', '%s', %i)
]]):format(name_or_ip, source, ts)
db_exec(stmt)
end
local function ban_player(name, source, reason, expires)
local ts = os.time()
local id = get_id(name)
local player = minetest.get_player_by_name(name)
-- initialise last position
local last_pos = ""
if player then
last_pos = minetest.pos_to_string(vector.round(player:getpos()))
end
-- id,name,source,created,reason,expires,u_source,u_reason,
-- u_date,active,last_pos
local stmt = ([[
INSERT INTO bans
VALUES ('%s','%s','%s','%s','%s','%s','','','','true','%s')
]]):format(id, name, source, ts, reason, expires, last_pos)
db_exec(stmt)
-- players: id,ban
stmt = ([[
UPDATE players SET ban = 'true' WHERE id = '%s'
]]):format(id)
db_exec(stmt)
local msg_k, msg_l
-- create kick & log messages
if expires ~= "" then
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)
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)
-- kick all names associated with the player
local records = find_records_by_id(id)
for i, v in ipairs(records) do
minetest.kick_player(v.name, msg_k)
end
end
local function set_version(str)
local stmt = ([[
INSERT INTO version
VALUES ('%s')
]]):format(str)
db_exec(stmt)
end
--[[
###########################
### Database: Updates ###
###########################
]]
local function update_login(player_name)
local ts = os.time()
local stmt = ([[
UPDATE playerdata SET last_login = %s WHERE name = '%s'
]]):format(ts, player_name)
db_exec(stmt)
end
local function unban_player(id, source, reason, name)
local ts = os.time()
local stmt = ([[
UPDATE players SET ban = 'false' WHERE id = '%i'
]]):format(id)
db_exec(stmt)
stmt = ([[
UPDATE bans SET
active = 'false',
u_source = '%s',
u_reason = '%s',
u_date = '%i'
WHERE id = '%i' AND active = 'true';
]]):format(source, reason, ts, id)
db_exec(stmt)
-- log event
minetest.log("action",
("[sban] %s unbanned by %s reason: %s"):format(name, source, reason))
end
local function reset_orphan_record(id)
local stmt = ([[
UPDATE players SET ban = 'false' WHERE id = '%s';
]]):format(id)
db_exec(stmt)
end
--[[
##################################
### Database: Delete Records ###
##################################
]]
local function del_ban_record(name)
local stmt = ([[
DELETE FROM bans WHERE name = '%s'
]]):format(name)
db_exec(stmt)
end
local function del_whitelist(name_or_ip)
local stmt = ([[
DELETE FROM whitelist WHERE name = '%s'
]]):format(name_or_ip)
db_exec(stmt)
end
--[[
#######################
### File Handling ###
#######################
]]
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 cont = f:read("*a")
f:close()
if not cont then
return false, "Unable to load xban2 database: Read failed"
end
local t = minetest.deserialize(cont)
if not t then
return false, "xban2 database: Deserialization failed"
end
return t
end
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
local function save_sql(txt)
local file = ie.io.open(WP.."/xban.sql", "a")
if file then
file:write(txt)
file:close()
end
end
local function del_sql()
ie.os.remove(WP.."/xban.sql")
end
--[[
##############
### Misc ###
##############
]]
-- initialise db version
if get_version() == nil then
set_version(db_version)
end
WL = get_whitelist()
local function import_xban(name, file_name)
local t, err = load_xban(file_name)
-- exit with error message
if not t then
return t, err
end
local id = next_id()
minetest.log("action", "processing "..#t.." records")
-- iterate the xban2 data
for i, e in ipairs(t) do
-- only process banned entries
if e.banned == true then
local names = {}
local ip = {}
local last_seen = e.last_seen
local last_pos = e.last_pos or ""
--local id = nil
local q
-- each entry in xban db contains a names field, both IP and names
-- are stored in this field, split into 2 tables
for k, v in pairs(e.names) do
if string.find(k, "%.") ~= nil then
table.insert(ip, k)
else
table.insert(names, k)
end
end
-- check for existing entry by name
local chk = true
for _, v in ipairs(names) do
q = ([[SELECT * FROM playerdata WHERE name = '%s']]):format(v)
local it, state = db:nrows(q)
local row = it(state)
if row then
chk = false
break
end
end
if chk then
-- process the entry
-- construct INSERT for players table
q = [[INSERT INTO players (ban) VALUES ('true');]]
db_exec(q)
-- If there are more names than IP's use the last entry for
-- the reamining entries IP. If there are more IP's use the
-- last name for the remaining entries
local ts = os.time()
if table.getn(names) > table.getn(ip) then
local tbl = table.getn(ip)
local idx
for ii, v in ipairs(names) do
idx = ii
if idx > tbl then idx = tbl end
-- id,name,ip,created,last_login
q = ([[
INSERT INTO playerdata
VALUES (%s,'%s','%s',%s,%s)
]]):format(id, v, ip[idx], ts, last_seen)
db_exec(q)
end
elseif table.getn(ip) > table.getn(names) then
local tbl = table.getn(names)
local idx
for ii, v in ipairs(ip) do
idx = ii
if idx > tbl then idx = tbl end
-- id,name,ip,created,last_login
q = ([[
INSERT INTO playerdata
VALUES (%s,'%s','%s',%s,%s)
]]):format(id, names[idx], v, ts, last_seen)
db_exec(q)
end
else
for ii, v in ipairs(names) do
-- id,name,ip,created,last_login
q = ([[
INSERT INTO playerdata
VALUES (%s,'%s','%s',%s,%s)
]]):format(id, v, ip[ii], ts, last_seen)
db_exec(q)
end
end
-- id,name,source,created,reason,expires,u_source,u_reason,
-- u_date,active,last_pos
-- convert position to string
if last_pos.y then
last_pos = minetest.pos_to_string(last_pos)
end
for _, v in ipairs(e.record) do
local expires = v.expires or ""
local reason = string.gsub(v.reason, "%'", "")
q = ([[
INSERT INTO bans
VALUES ('%s','%s','%s','%s','%s','%s','','','','%s','%s')
]]):format(id, names[1], v.source, v.time,
reason, expires, e.banned, last_pos)
db_exec(q)
end
id = id + 1
end
end
end
end
local function import_ipban(source)
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 chk = true
local q = ([[SELECT * FROM
playerdata WHERE name = '%s']]):format(name)
local it, state = db:nrows(q)
local row = it(state)
if row then
chk = false
end
if chk then
-- create player entry
create_entry(name, ip)
end
-- check for existing ban
local r = is_banned(name)
if #r == 0 then
-- create ban entry - name,source,reason,expires
ban_player(name, source, "imported from ipban.txt", '')
end
end
end
end
local function sql_string(id, entry)
local names = {}
local ip = {}
local last_seen = entry.last_seen
local last_pos = entry.last_pos or ""
local ts = os.time()
-- names field includes both IP and names data, sort into 2 tables
for k, v in pairs(entry.names) do
if string.find(k, "%.") ~= nil then
table.insert(ip, k)
else
table.insert(names, k)
end
end
-- construct INSERT for players table based on ban status
local q = ("INSERT INTO players VALUES ('%s','%s');\n"
):format(id, entry.banned)
-- case: more names than IP's uses the last entry for reamining names
if #names > #ip then
local t = #ip
local idx
for i, v in ipairs(names) do
idx = i
if idx > t then idx = t end
-- id,name,ip,created,last_login
q = q..("INSERT INTO playerdata VALUES ('%s','%s','%s','%s','%s');\n"
):format(id, v, ip[idx], ts, last_seen)
end
-- case: more ip's than names uses last entry for remaining ip's
elseif #ip > #names then
local t = #names
local idx
for i, v in ipairs(ip) do
idx = i
if idx > t then idx = t end
-- id,name,ip,created,last_login
q = q..("INSERT INTO playerdata VALUES ('%s','%s','%s','%s','%s');\n"
):format(id, names[idx], v, ts, last_seen)
end
-- case: number of ip's and names is equal
else
for i, v in ipairs(names) do
-- id,name,ip,created,last_login
q = q..("INSERT INTO playerdata VALUES ('%s','%s','%s','%s','%s');\n"
):format(id, v, ip[i], ts, last_seen)
end
end
if entry.reason then
-- convert position
if last_pos.y then
last_pos = vector.round(last_pos)
last_pos = minetest.pos_to_string(last_pos)
end
-- id,name,source,created,reason,expires,u_source,u_reason,u_date,active,last_pos
for i, v in ipairs(entry.record) do
local expires = v.expires or ""
local reason = string.gsub(v.reason, "%'", "")
reason = string.gsub(reason, "%:%)", "")
q = q..("INSERT INTO bans VALUES ('%s','%s','%s','%i','%s','%s','','','','%s','%s');\n"
):format(id, names[1], v.source, v.time, reason, expires, entry.banned, last_pos)
end
end
return q
end
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 = load_xban(filename)
local id = next_id()
-- reverse the contents with #entries/2
for i = 1, math.floor(#dbi / 2) do
local tmp = dbi[i]
dbi[i] = dbi[#dbi - i + 1]
dbi[#dbi - i + 1] = tmp
end
-- add create tables string
save_sql(createDb)
-- add single transaction
save_sql("BEGIN;\n")
-- process records
for i = #dbi, 1, - 1 do
-- contains data?
if dbi[i] then
local str = sql_string(id, dbi[i]) -- sql statement
save_sql(str)
dbi[i] = nil -- housekeeping
id = id + 1
end
end
-- close transaction
save_sql("END;")
end
-- Export the database back to xban db format
local function export_xban()
-- so long, thanks for trying it :P
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
-- players
local q = [[SELECT * FROM players;]]
for row in db:nrows(q) do
local b = false
if row.ban == 'true' then b = true end
xport[row.id] = {
banned = b
}
end
-- playerdata
for i,v in ipairs(xport) do
local name, ip = {}, {}
xport[i].names = {}
q = ([[SELECT * FROM playerdata
WHERE id = '%i']]):format(i)
for row in db:nrows(q) do
if not name[row.name] then
name[row.name] = true
end
if not ip[row.ip] then
ip[row.ip] = true
end
xport[i].last_seen = row.last_login
end
for key,val in pairs(name) do
xport[i].names[key] = val
end
for key,val in pairs(ip) do
xport[i].names[key] = val
end
end
-- bans
for i,v in ipairs(xport) do
if xport[i].banned == true then
local t = {}
q = ([[SELECT * FROM bans WHERE id = '%i';]]):format(i)
for row in db:nrows(q) do
t[#t+1] = {
time = row.created,
source = row.source,
reason = row.reason
}
if row.active == 'true' then
xport[i].last_pos = minetest.string_to_pos(row.last_pos)
end
end
xport[i].record = t
end
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
local function hotlistp(name)
for _, v in ipairs(hotlist) do
if v == name then return end
end
table.insert(hotlist, name)
if #hotlist > 10 then
table.remove(hotlist, 1)
end
end
--[[
###########
## GUI ##
###########
]]
local state = {}
local FORMNAME = "sban:main"
local function get_state(name)
local s = state[name]
if not s then
s = {
list = {},
hlist = {},
index = -1,
info = "Select an entry from the list\n or use search",
banned = false,
bans = nil,
multi = false,
page = 1,
flag = false
}
state[name] = s
end
return s
end
local function create_info(entry)
if not entry then
return "something went wrong!\n Please reselct the entry."
end
local str = "Banned by: "..entry.source.."\n"
.."Active: "..entry.active.."\n"
.."When: "..hrdf(entry.created).."\n"
if entry.expires ~= '' then
str = str.."Expires: "..hrdf(entry.expires).."\n"
end
str = str .."Reason: "
-- Word wrap
local words = entry.reason:split(" ")
local l,ctr = 40,8 -- character limit
for i,word in ipairs(words) do
local wl = word:len()
if ctr + wl < l then
str = str..word.." "
ctr = ctr + (wl + 1)
else
str = str.."\n"..word.." "
ctr = wl + 1
end
end
if entry.active == "false" then
str = str.."\nUnbanned by: "..entry.u_source.."\n"
.."Reason: "..entry.u_reason.."\n"
.."When: "..hrdf(entry.u_date).."\n"
end
return str
end
local function getformspec(name)
local fs = state[name]
local f
local list = fs.list
local bgimg = ""
if default and default.gui_bg_img then
bgimg = default.gui_bg_img
end
f = "size[8,6.6]"
..bgimg
.."field[0.3,0.4;4.5,0.5;search;;]"
.."field_close_on_enter[search;false]"
.."button[4.5,0.1;1.5,0.5;find;Find]"
if #fs.list > 0 then
f = f.."textlist[0,0.9;2.4,3.6;plist;"
for i,v in ipairs(list) do
f = f..v..","
end
f = f:sub(1, f:len() - 1)
f = f..";"..fs.index.."]"
end
f = f.."field[0.3,6.5;4.5,0.5;reason;Reason:;]"
.."field_close_on_enter[reason;false]"
if fs.multi == true then
f = f.."image_button[6,0.1;0.5,0.5;ui_left_icon.png;left;]"
.."image_button[7,0.1;0.5,0.5;ui_right_icon.png;right;]"
if fs.page > 9 then
f = f.."label[6.50,0.09;"..fs.page.."]"
else
f = f.."label[6.55,0.09;"..fs.page.."]"
end
end
f = f.."label[2.6,0.9;"..fs.info.."]"
if fs.banned then
f = f.."button[4.5,6.2;1.5,0.5;unban;Unban]"
else
f = f
.."field[0.3,5.5;2.6,0.3;duration;Duration:;"..def_duration.."]"
.."field_close_on_enter[duration;false]"
.."button[4.5,6.2;1.5,0.5;ban;Ban]"
.."button[6,6.2;2,0.5;tban;Temp Ban]"
end
return f
end
local function update_state(name, selected)
local fs = get_state(name)
local id = get_id(selected)
fs.bans = list_bans(id)
local info = "Ban records: "..#fs.bans.."\n"
fs.banned = qbc(id)
fs.multi = false
if #fs.bans == 0 then
info = info.."Player has no ban records!"
else
if not fs.flag then
fs.page = #fs.bans
fs.flag = true
end
if fs.page > #fs.bans then fs.page = #fs.bans end
info = info..create_info(fs.bans[fs.page])
end
fs.info = info
if #fs.bans > 1 then
fs.multi = true
end
end
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
minetest.log("warning",
"[sban] Received fields from unauthorized user: "..name)
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
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.bans 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
ban_player(selected, name, ESC(fields.reason), '')
local q = qbc(id)
if not (q and active_ban_record(id)) then
fs.info = "Warning: failed to ban "..selected
end
end
elseif fields.unban then
unban_player(id, name, ESC(fields.reason), selected)
fs.bans = list_bans(id)
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()
ban_player(selected, name, ESC(fields.reason), t)
local q = qbc(id)
if not (q and active_ban_record(id)) then
fs.info = "Warning: failed to ban "..selected
end
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 Commands ###
###########################
]]
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+(.+)")
-- check params are present
if not (player_name and reason) then
return false, "Usage: /ban <player> <reason>"
end
-- protect owner
if player_name == owner then
return false, "Insufficient privileges!"
end
-- banned player?
local id = get_id(player_name)
if qbc(id) then
if active_ban_record(id) then
return true, ("%s is already banned!"):format(player_name)
else
reset_orphan_record(id)
minetest.log("info",
"[sban] cleared orphaned ban in players table for "
..player_name)
end
end
-- limit ban?
local expires = ''
if expiry ~= nil then
expires = parse_time(expiry) + os.time()
end
-- handle known/unknown players dependant on privs
local q
if id then
-- existing player
-- Params: name, source, reason, expires
ban_player(player_name, name, reason, expires)
q = qbc(id)
if q and active_ban_record(id) then
return true, ("Banned %s."):format(player_name)
else
minetest.log("error", "Failed to ban "..player_name)
return false, ("Failed to ban %s"):format(player_name)
end
else
local privs = minetest.get_player_privs(name)
-- assert normal behaviour without ban_admin priv
if not privs.ban_admin then
return false, "Player doesn't exist!"
end
-- create entry before ban
id = create_entry(player_name, "0.0.0.0") -- arbritary ip
ban_player(player_name, name, reason, expires)
q = qbc(id)
if q and active_ban_record(id) then
return true, ("Banned nonexistent player %s."):format(player_name)
else
minetest.log("error", "Failed to ban "..player_name)
return false, ("Failed to ban %s"):format(player_name)
end
end
end
})
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, "xban2 dumped to xban.sql file!"
end
})
minetest.register_chatcommand("ban_dbx", {
description = "export db to xban2 format",
privs = {server = true},
func = function(name)
export_xban()
return true, "dumped db to xban2 file!"
end
})
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(name, filename)
msg = err
if res then
msg = filename.." imported!"
end
end
return true, msg
end
})
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_record <player>"
end
del_ban_record(player_name)
minetest.log("action",
"ban records for "..player_name.." deleted by "..name)
return true, player_name.." ban records deleted!"
end
})
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 then
return false, "Useage: /ban_record <player_name>"
end
-- get target and source privs
local id = get_id(playername)
local target = find_records_by_id(id)
local source = minetest.get_player_privs(name)
local chk = false
-- If the target has server privs on any account
-- do NOT allow record to be shown unless source
-- has server priv.
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
if not source.server and chk then
return false, "Insufficient privileges!"
end
display_record(name, playername)
return true
end
})
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(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(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
})
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
return false, "Usage: /tempban <player> <time> <reason>"
end
if player_name == owner then
return false, "Insufficient privileges!"
end
-- is player already banned?
local id = get_id(player_name)
local q = qbc(id)
if q then
if active_ban_record(id) then
return true, ("%s is already banned!"):format(player_name)
else
reset_orphan_record(id)
minetest.log("info",
"[sban] cleared orphaned ban in players table for "
..player_name)
end
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
local r = find_records(player_name)
if #r > 0 then
-- existing player
ban_player(player_name, name, reason, expires)
q = qbc(id)
if q and active_ban_record(id) then
return true, ("Banned %s until %s."):format(
player_name, os.date("%c", expires))
else
minetest.log("error", "Failed to ban "..player_name)
return false, ("Failed to ban %s"):format(player_name)
end
else
local privs = minetest.get_player_privs(name)
-- assert normal behaviour without server priv
if not privs.ban_admin then
return false, "Player doesn't exist!"
end
-- create entry before ban
create_entry(player_name, "0.0.0.0")
ban_player(player_name, name, reason, expires)
q = qbc(id)
if q then
return true, ("Banned nonexistent player %s until %s."
):format(player_name, os.date("%c", expires))
else
minetest.log("error", "Failed to ban "..player_name)
return false, ("Failed to ban %s"):format(player_name)
end
end
end,
})
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)
local q = qbc(id)
if not q then
return false, ("No active ban record for "..player_name)
end
local bans = list_bans(id) -- get ban records
-- look for the active ban
for i, v in ipairs(bans) do
if v.active then
unban_player(id, name, reason, player_name)
q = qbc(id)
if not q then
return true, ("Unbanned %s."):format(v.name)
elseif q then
minetest.log("error", "[sban] Failed to unban "..
player_name)
return false
end
end
end
return false, "no record found for "..player_name
end,
})
minetest.register_chatcommand("bang", {
description = "Launch sban gui",
privs = {ban = true},
func = function(name)
state[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 callbacks ###
############################
]]
minetest.register_on_shutdown(function()
db:close()
end)
minetest.register_on_prejoinplayer(function(name, ip)
-- check player isn't whitelisted
if WL[name] or WL[ip] then
minetest.log("action", "[sban] "..
name.." whitelisted entry permits login")
return
end
-- Attempt to retieve id
local id = get_id(name) or get_id(ip)
if id == nil then return end -- no record
if qbc(id) then
-- Check
if not active_ban_record(id) then
-- partial record - remove
reset_orphan_record(id)
minetest.log("info",
"[sban] cleared orphaned ban in players table for "
..name)
return
end
else
return -- not banned
end
-- Retrieve player record
local data = check_ban(id)
local date
-- check for ban expiry
if data and type(data.expires) == "number" and data.expires ~= 0 then
-- temp ban
if os.time() > data.expires then
-- clear temp ban
unban_player(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)
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
local ip = minetest.get_player_ip(name)
if not ip then return end
local record = find_records(name)
local ip_record
hotlistp(name)
-- check for player name entry
if #record == 0 then
-- no records, check for ip
ip_record = find_records(ip)
if #ip_record == 0 then
-- create new entry
create_entry(name, ip)
return
else
-- add record [new name]
add_player(ip_record[1].id, name, ip)
return
end
else
-- check for ip record
ip_record = find_records(ip)
if #ip_record == 0 then
-- add record [player is using a new ip]
add_player(record[1].id, name, ip)
return
end
-- update record
update_login(name)
end
end)