Compare commits

...

10 Commits

Author SHA1 Message Date
David Leal
e870b8d1d6
Add a license file (#27) 2023-09-11 20:49:22 +02:00
luk3yx
c850d11a3c
Use JSON in xban.db (#26)
This should prevent bans database from resetting with a "function has more than 65536 constants" error. Older databases should still be loaded correctly.

Also makes use of minetest.safe_file_write to avoid data corruption.
2023-09-07 17:38:26 +02:00
sfan5
d2cda4f73a Improve behavior of GUI search field 2020-06-02 13:09:52 +02:00
sfan5
37cdbf014e Fix list of names in log message upon (un-)banning 2019-11-05 16:49:50 +01:00
Thomas Rudin
e937f5ff67 Add /xban_cleanup command to purge unbanned entries (#20)
Add documentation for /xban_cleanup
2019-06-06 18:54:46 +02:00
Diego Martínez
3b70045365 Update to 5.0 auth. 2019-02-12 16:53:03 -03:00
SaKeL
3270942a0f Replace deprecated minetest.setting_* functions (#16) 2018-07-22 13:20:35 +02:00
Diego Martínez
6738109c15 Create README.md 2017-04-24 04:44:47 -03:00
Diego Martínez
89303b4a01 Add .luacheckrc and fix warnings. 2017-02-17 02:30:30 -03:00
Diego Martínez
97fb251ad9 Better diagnostics in case of DB load error. 2016-12-11 17:57:29 -03:00
8 changed files with 240 additions and 31 deletions

7
.luacheckrc Normal file
View File

@ -0,0 +1,7 @@
unused_args = false
allow_defined_top = true
read_globals = {
"minetest",
}

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
BSD 2-Clause License
Copyright (c) 2014-2023, Diego Martínez
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

112
README.md Normal file
View File

@ -0,0 +1,112 @@
# Extended Ban Mod for Minetest
This mod attempts to be an improvement to Minetest's ban system.
* It supports normal bans and temporary bans (from 60 seconds up to the end of
time, with 1 second granularity).
* Records and joins all accounts using the same IP address and several IP
addresses using the same name into a single record, and can ban/unban them as
a single user.
* Can ban offline players if you know their IP or username.
* Holds a record of bans for each user, so moderators and administrators can
consult it to know if a player is a repeat offender.
* Does not modify the default ban database in any way (`ipban.txt').
* Has an API to ban and check the ban database to allows other mods to manage
users (for example, anticheat mods).
## Chat commands
The mod provides the following chat commands. All commands require the `ban`
privilege.
### `xban`
Bans a player permanently.
**Usage:** `/xban <player_or_ip> <reason>`
**Example:** `/xban 127.0.0.1 Some reason.`
### `xtempban`
Bans a player temporarily.
**Usage:** `/xtempban <player_or_ip> <time> <reason>`
The `time` parameter is a string in the format `<count><unit>` where `<unit>`
is one of `s` for seconds, `m` for minutes, `h` for hours, `D` for days, `W`
for weeks, `M` for months, or `Y` for years. If the unit is omitted, it is
assumed to mean seconds. For example, `42s` means 42 seconds, `1337m` 1337
minutes, and so on. You can chain more than one such group and they will add
up. For example, `1Y3M3D7h` will ban for 1 year, 3 months, 3 days and 7 hours.
**Example:** `/xtempban Joe 3600 Some reason.`
### `xunban`
Unbans a player.
**Usage:** `/xunban <player_or_ip>`
**Example:** `/xunban Joe`
### `xban_record`
Shows the ban record on chat.
**Usage:** `/xban_record <player_or_ip>`
This prints one ban entry per line, with the time the ban came into effect,
the expiration time (if applicable), the reason, and the source of the ban.
The record is printed to chat with one entry per line.
**Example:** `/xban_record Joe`
### `xban_wl`
Manages the whitelist.
**Usage:** `/xban_wl (add|del|get) <player_or_ip>`
Whitelisted players are allowed on the server even if it's otherwise marked
as banned. This is useful to only allow certain users from shared computers,
for example.
The `add` subcommand adds the player to the whitelist. The `del` subcommand
removes the player from the whitelist. The `get` subcommand checks if the
player is in the whitelist, and prints the status to chat.
**Example:** `/xban_record add Jane`
### `xban_gui`
Shows a form to consult the database interactively.
**Usage:** `/xban_gui`
## Administrator commands
The following commands require the `server` privilege, so they are only
available to server administrators.
### `xban_dbi`
Imports ban entries from other database formats.
**Usage:** `/xban_dbi <importer>`
The `importer` argument specifies from which database to import. These are
the supported import plugins at the time of writing:
* `minetest`: Import entries from Minetest's ban list (`ipban.txt`).
* `v1`: Old format used by xban (`players.iplist`).
* `v2`: Old format used by xban (`players.iplist.v2`).
**Example:** `/xban_dbi minetest`
### `xban_cleanup`
Removes all non-banned entries from the xban db.
**Usage:** `/xban_cleanup`

View File

@ -11,13 +11,13 @@ minetest.register_chatcommand("xban_dbi", {
privs = { server=true }, privs = { server=true },
func = function(name, params) func = function(name, params)
if params == "--list" then if params == "--list" then
local names = { } local importers = { }
for name in pairs(xban.importers) do for importer in pairs(xban.importers) do
table.insert(names, name) table.insert(importers, importer)
end end
minetest.chat_send_player(name, minetest.chat_send_player(name,
("[xban] Known importers: %s"):format( ("[xban] Known importers: %s"):format(
table.concat(names, ", "))) table.concat(importers, ", ")))
return return
elseif not xban.importers[params] then elseif not xban.importers[params] then
minetest.chat_send_player(name, minetest.chat_send_player(name,

View File

@ -9,7 +9,7 @@ local ESC = minetest.formspec_escape
local function make_list(filter) local function make_list(filter)
filter = filter or "" filter = filter or ""
local list, n, dropped = { }, 0, false local list, n, dropped = { }, 0, false
for k in pairs(minetest.auth_table) do for k in minetest.get_auth_handler().iterate() do
if strfind(k, filter, 1, true) then if strfind(k, filter, 1, true) then
if n >= MAXLISTSIZE then if n >= MAXLISTSIZE then
dropped = true dropped = true
@ -61,7 +61,8 @@ local function make_fs(name)
"size[16,12]", "size[16,12]",
"label[0,-.1;Filter]", "label[0,-.1;Filter]",
"field[1.5,0;12.8,1;filter;;"..ESC(filter).."]", "field[1.5,0;12.8,1;filter;;"..ESC(filter).."]",
"button[14,-.3;2,1;search;Search]", "field_close_on_enter[filter;false]",
"button[14,-.3;2,1;search_submit;Search]",
} }
local fsn = #fs local fsn = #fs
fsn=fsn+1 fs[fsn] = format("textlist[0,.8;4,9.3;player;%s;%d;0]", fsn=fsn+1 fs[fsn] = format("textlist[0,.8;4,9.3;player;%s;%d;0]",
@ -122,7 +123,7 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end end
return return
end end
if fields.search then if fields.search_submit or fields.filter then
local filter = fields.filter or "" local filter = fields.filter or ""
state.filter = filter state.filter = filter
state.list = make_list(filter) state.list = make_list(filter)

View File

@ -8,19 +8,19 @@ function xban.importers.v2()
local text = f:read("*a") local text = f:read("*a")
f:close() f:close()
local db = minetest.deserialize(text) local db = minetest.deserialize(text)
for _, e in ipairs(db) do for _, ent in ipairs(db) do
for name in pairs(e.names) do for name in pairs(ent.names) do
local entry = xban.find_entry(name, true) local entry = xban.find_entry(name, true)
if entry.source ~= "xban:importer_v2" then if entry.source ~= "xban:importer_v2" then
for nm in pairs(e.names) do for nm in pairs(e.names) do
entry.names[nm] = true entry.names[nm] = true
end end
if e.banned then if ent.banned then
entry.banned = true entry.banned = true
entry.reason = e.banned entry.reason = e.banned
entry.source = "xban:importer_v2" entry.source = "xban:importer_v2"
entry.time = e.time entry.time = ent.time
entry.expires = e.expires entry.expires = ent.expires
table.insert(entry.record, { table.insert(entry.record, {
source = entry.source, source = entry.source,
reason = entry.reason, reason = entry.reason,

View File

@ -9,9 +9,9 @@ local tempbans = { }
local DEF_SAVE_INTERVAL = 300 -- 5 minutes local DEF_SAVE_INTERVAL = 300 -- 5 minutes
local DEF_DB_FILENAME = minetest.get_worldpath().."/xban.db" local DEF_DB_FILENAME = minetest.get_worldpath().."/xban.db"
local DB_FILENAME = minetest.setting_get("xban.db_filename") local DB_FILENAME = minetest.settings:get("xban.db_filename")
local SAVE_INTERVAL = tonumber( local SAVE_INTERVAL = tonumber(
minetest.setting_get("xban.db_save_interval")) or DEF_SAVE_INTERVAL minetest.settings:get("xban.db_save_interval")) or DEF_SAVE_INTERVAL
if (not DB_FILENAME) or (DB_FILENAME == "") then if (not DB_FILENAME) or (DB_FILENAME == "") then
DB_FILENAME = DEF_DB_FILENAME DB_FILENAME = DEF_DB_FILENAME
@ -24,7 +24,6 @@ local function make_logger(level)
end end
local ACTION = make_logger("action") local ACTION = make_logger("action")
local INFO = make_logger("info")
local WARNING = make_logger("warning") local WARNING = make_logger("warning")
local ERROR = make_logger("error") local ERROR = make_logger("error")
@ -42,6 +41,14 @@ local function parse_time(t) --> secs
return secs return secs
end end
local function concat_keys(t, sep)
local keys = {}
for k, _ in pairs(t) do
keys[#keys + 1] = k
end
return table.concat(keys, sep)
end
function xban.find_entry(player, create) --> entry, index function xban.find_entry(player, create) --> entry, index
for index, e in ipairs(db) do for index, e in ipairs(db) do
for name in pairs(e.names) do for name in pairs(e.names) do
@ -113,7 +120,7 @@ function xban.ban_player(player, source, expires, reason) --> bool, err
end end
ACTION("%s bans %s until %s for reason: %s", source, player, ACTION("%s bans %s until %s for reason: %s", source, player,
date, reason) date, reason)
ACTION("Banned Names/IPs: %s", table.concat(e.names, ", ")) ACTION("Banned Names/IPs: %s", concat_keys(e.names, ", "))
return true return true
end end
@ -133,7 +140,7 @@ function xban.unban_player(player, source) --> bool, err
e.expires = nil e.expires = nil
e.time = nil e.time = nil
ACTION("%s unbans %s", source, player) ACTION("%s unbans %s", source, player)
ACTION("Unbanned Names/IPs: %s", table.concat(e.names, ", ")) ACTION("Unbanned Names/IPs: %s", concat_keys(e.names, ", "))
return true return true
end end
@ -299,11 +306,11 @@ minetest.register_chatcommand("xban_wl", {
local cmd, plname = params:match("%s*(%S+)%s*(%S+)") local cmd, plname = params:match("%s*(%S+)%s*(%S+)")
if cmd == "add" then if cmd == "add" then
xban.add_whitelist(plname, name) xban.add_whitelist(plname, name)
ACTION("%s adds %s to whitelist", source, plname) ACTION("%s adds %s to whitelist", name, plname)
return true, "Added to whitelist: "..plname return true, "Added to whitelist: "..plname
elseif cmd == "del" then elseif cmd == "del" then
xban.remove_whitelist(plname) xban.remove_whitelist(plname)
ACTION("%s removes %s to whitelist", source, plname) ACTION("%s removes %s to whitelist", name, plname)
return true, "Removed from whitelist: "..plname return true, "Removed from whitelist: "..plname
elseif cmd == "get" then elseif cmd == "get" then
local e = xban.get_whitelist(plname) local e = xban.get_whitelist(plname)
@ -316,6 +323,7 @@ minetest.register_chatcommand("xban_wl", {
end, end,
}) })
local function check_temp_bans() local function check_temp_bans()
minetest.after(60, check_temp_bans) minetest.after(60, check_temp_bans)
local to_rm = { } local to_rm = { }
@ -336,18 +344,12 @@ end
local function save_db() local function save_db()
minetest.after(SAVE_INTERVAL, save_db) minetest.after(SAVE_INTERVAL, save_db)
local f, e = io.open(DB_FILENAME, "wt")
db.timestamp = os.time() db.timestamp = os.time()
if f then local contents = assert(xban.serialize_db(db))
local ok, err = f:write(xban.serialize(db)) local ok = minetest.safe_file_write(DB_FILENAME, contents)
if not ok then if not ok then
WARNING("Unable to save database: %s", err) ERROR("Unable to save database")
end end
else
WARNING("Unable to save database: %s", e)
end
if f then f:close() end
return
end end
local function load_db() local function load_db()
@ -361,10 +363,10 @@ local function load_db()
WARNING("Unable to load database: %s", "Read failed") WARNING("Unable to load database: %s", "Read failed")
return return
end end
local t = minetest.deserialize(cont) local t, e2 = xban.deserialize_db(cont)
if not t then if not t then
WARNING("Unable to load database: %s", WARNING("Unable to load database: %s",
"Deserialization failed") "Deserialization failed: "..(e2 or "unknown error"))
return return
end end
db = t db = t
@ -376,6 +378,30 @@ local function load_db()
end end
end end
minetest.register_chatcommand("xban_cleanup", {
description = "Removes all non-banned entries from the xban db",
privs = { server=true },
func = function(name, params)
local old_count = #db
local i = 1
while i <= #db do
if not db[i].banned then
-- not banned, remove from db
table.remove(db, i)
else
-- banned, hold entry back
i = i + 1
end
end
-- save immediately
save_db()
return true, "Removed " .. (old_count - #db) .. " entries, new db entry-count: " .. #db
end,
})
minetest.register_on_shutdown(save_db) minetest.register_on_shutdown(save_db)
minetest.after(SAVE_INTERVAL, save_db) minetest.after(SAVE_INTERVAL, save_db)
load_db() load_db()

View File

@ -27,5 +27,44 @@ local function my_serialize_2(t, level)
end end
function xban.serialize(t) function xban.serialize(t)
minetest.log("warning", "[xban2] xban.serialize() is deprecated")
return "return {\n"..my_serialize_2(t, 1).."\n}" return "return {\n"..my_serialize_2(t, 1).."\n}"
end end
-- JSON doesn't allow combined string+number keys, this function moves any
-- number keys into an "entries" table
function xban.serialize_db(t)
local res = {}
local entries = {}
for k, v in pairs(t) do
if type(k) == "number" then
entries[k] = v
else
res[k] = v
end
end
res.entries = entries
return minetest.write_json(res, true)
end
function xban.deserialize_db(s)
if s:sub(1, 1) ~= "{" then
-- Load legacy databases
return minetest.deserialize(s)
end
local res, err = minetest.parse_json(s)
if not res then
return nil, err
end
-- Remove all "null"s added by empty tables
for i, entry in ipairs(res.entries or {}) do
entry.names = entry.names or {}
entry.record = entry.record or {}
res[i] = entry
end
res.entries = nil
return res
end