diff options
Diffstat (limited to 'xban2')
| -rw-r--r-- | xban2/bower.json | 15 | ||||
| -rw-r--r-- | xban2/dbimport.lua | 38 | ||||
| -rw-r--r-- | xban2/doc/API.md | 32 | ||||
| -rw-r--r-- | xban2/doc/dbformat.txt | 45 | ||||
| -rw-r--r-- | xban2/gui.lua | 90 | ||||
| -rw-r--r-- | xban2/importers/minetest.lua | 29 | ||||
| -rw-r--r-- | xban2/importers/v1.lua | 33 | ||||
| -rw-r--r-- | xban2/importers/v2.lua | 35 | ||||
| -rw-r--r-- | xban2/init.lua | 331 | ||||
| -rw-r--r-- | xban2/serialize.lua | 31 | 
10 files changed, 679 insertions, 0 deletions
diff --git a/xban2/bower.json b/xban2/bower.json new file mode 100644 index 0000000..1e1576d --- /dev/null +++ b/xban2/bower.json @@ -0,0 +1,15 @@ +{ +  "name": "xban2", +  "description": "Ban system extension with support for temporary bans.", +  "keywords": [ +    "ban", +    "administration", +    "system", +    "server" +  ], +  "homepage": "http://github.com/kaeza/minetest-xban2.git", +  "authors": [ +    "Diego MartÃnez <lkaezadl3@yahoo.com>" +  ], +  "license": "BSD 2-Clause" +} diff --git a/xban2/dbimport.lua b/xban2/dbimport.lua new file mode 100644 index 0000000..ec9293d --- /dev/null +++ b/xban2/dbimport.lua @@ -0,0 +1,38 @@ + +xban.importers = { } + +dofile(xban.MP.."/importers/minetest.lua") +dofile(xban.MP.."/importers/v1.lua") +dofile(xban.MP.."/importers/v2.lua") + +minetest.register_chatcommand("xban_dbi", { +	description = "Import old databases", +	params = "<importer>", +	privs = { server=true }, +	func = function(name, params) +		if params == "--list" then +			local names = { } +			for name in pairs(xban.importers) do +				table.insert(names, name) +			end +			minetest.chat_send_player(name, +			  ("[xban] Known importers: %s"):format( +			  table.concat(names, ", "))) +			return +		elseif not xban.importers[params] then +			minetest.chat_send_player(name, +			  ("[xban] Unknown importer `%s'"):format(params)) +			minetest.chat_send_player(name, "[xban] Try `--list'") +			return +		end +		local f = xban.importers[params] +		local ok, err = f() +		if ok then +			minetest.chat_send_player(name, +			  "[xban] Import successfull") +		else +			minetest.chat_send_player(name, +			  ("[xban] Import failed: %s"):format(err)) +		end +	end, +}) diff --git a/xban2/doc/API.md b/xban2/doc/API.md new file mode 100644 index 0000000..bee7c42 --- /dev/null +++ b/xban2/doc/API.md @@ -0,0 +1,32 @@ + +## Extended Ban Mod API + +### ban_player + +`xban.ban_player(player_or_ip, source, expires, reason)` + +Ban a player and all of his/her alternative names and IPs. + +#### Arguments: + +* `player_or_ip` - Player to search for and ban. See note 1 below. +* `source` - Source of the ban. See note 2 below. +* `expires` - Time at which the ban expires. If nil, ban is permanent. +* `reason` - Reason for ban. + +### unban_player + +`xban.unban_player(player_or_ip, source)` + +Unban a player and all of his/her alternative names and IPs. + +#### Arguments: + +* `player_or_ip` - Player to search for and unban. +* `source` - Source of the ban. See note 2 below. + +### Notes + +* 1: If player is currently online, all his accounts are kicked. +* 2: Mods using the xban API are advised to use the `"modname:source"` +format for `source` (for example: `"anticheat:main"`). diff --git a/xban2/doc/dbformat.txt b/xban2/doc/dbformat.txt new file mode 100644 index 0000000..71b25a5 --- /dev/null +++ b/xban2/doc/dbformat.txt @@ -0,0 +1,45 @@ + +Database is a regular Lua script that returns a table. + +Table has a single named field `timestamp' containing the time_t the +DB was last saved. It's not used in the mod and is only meant for +external use (I don't find filesystem timestamps too reliable). + +Next is a simple array (number indices) of entries. + +Each entry contains following fields: + +[1] = { +	-- Names/IPs associated with this entry +	names = { +		["foo"] = true, +		["bar"] = true, +		["123.45.67.89"] = true, +	}, +	banned = true,      -- Whether this user is banned +	                    -- Other fields do not apply if false +	time = 12341234,    -- Time of last ban (*1) +	expires = 43214321  -- Time at which ban expires (*2) +	                    -- If nil, permanent ban +	reason = "asdf",    -- Reason for ban +	source = "qwerty",  -- Source of ban (*2) +	record = { +		[1] = { +			source = "asdf", +			reason = "qwerty", +			time = 12341234, +			expires = 43214321, +		}, +		[1] = { +			source = "asdf", +			reason = "Unbanned",  -- When unbanned +			time = 12341234, +		}, +	}, +} + +Notes: +(*1)  All times are expressed in whatever unit `os.time()' uses +      (`time_t' on most (all?) systems). +(*2)  Mods using the xban API are advised to use the "modname:source" +      format for `source' (for example: "anticheat:main"). diff --git a/xban2/gui.lua b/xban2/gui.lua new file mode 100644 index 0000000..544f3be --- /dev/null +++ b/xban2/gui.lua @@ -0,0 +1,90 @@ + +local FORMNAME = "xban2:main" + +local states = { } + +local table_insert, table_concat = +      table.insert, table.concat + +local ESC = minetest.formspec_escape + +local function make_fs(name) +	local state = states[name] +	if not state then return end +	local list, index, filter = state.list, state.index, state.filter +	if index > #list then +		index = #list +	end +	local fs = { +		"size[10,8]", +		"label[0.5,0.6;Filter]", +		"field[1.5,0.5;6,2;filter;;"..ESC(filter).."]", +		"button[7.5,0.5;2,1;search;Search]", +	} +	table_insert(fs, +			("textlist[0.5,2;3,5.5;player;%s;%d;0]"): +			format(table_concat(list, ","), index)) +	local record_name = list[index] +	if record_name then +		local record, err = xban.get_record(record_name) +		if record then +			local reclist = { } +			for _, r in ipairs(record) do +				table_insert(reclist, ESC(r)) +			end +			table_insert(fs, +					("textlist[4,2;5,5.5;entry;%s;0;0]"): +					format(table_concat(reclist, ","))) +		else +			table_insert(fs, +					"textlist[4,2;5,5.5;entry;"..ESC(err)..";0]") +		end +	end +	return table_concat(fs) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) +	if formname ~= FORMNAME then return end +	local name = player:get_player_name() +	local state = states[name] +	if fields.player then +		local t = minetest.explode_textlist_event(fields.player) +		if (t.type == "CHG") or (t.type == "DCL") then +			state.index = t.index +			minetest.show_formspec(name, FORMNAME, make_fs(name)) +		end +		return +	end +	if fields.search then +		local filter = fields.filter or "" +		state.filter = filter +		local list = { } +		state.list = list +		for k in pairs(minetest.auth_table) do +			if k:find(filter, 1, true) then +				table_insert(list, k) +			end +		end +		table.sort(list) +		minetest.show_formspec(name, FORMNAME, make_fs(name)) +	end +end) + +minetest.register_chatcommand("xban_gui", { +	description = "Show XBan GUI", +	params = "", +	func = function(name, params) +		local state = states[name] +		if not state then +			state = { index=1, filter="" } +			states[name] = state +			local list = { } +			state.list = list +			for k in pairs(minetest.auth_table) do +				table_insert(list, k) +			end +			table.sort(list) +		end +		minetest.show_formspec(name, FORMNAME, make_fs(name)) +	end, +}) diff --git a/xban2/importers/minetest.lua b/xban2/importers/minetest.lua new file mode 100644 index 0000000..e19c3c6 --- /dev/null +++ b/xban2/importers/minetest.lua @@ -0,0 +1,29 @@ + +function xban.importers.minetest() +	local f, e = io.open(minetest.get_worldpath().."/ipban.txt") +	if not f then +		return false, "Unable to open `ipban.txt': "..e +	end +	for line in f:lines() do +		local ip, name = line:match("([^|]+)%|(.+)") +		if ip and name then +			local entry +			entry = xban.find_entry(ip, true) +			entry.banned = true +			entry.reason = "Banned in `ipban.txt'" +			entry.names[name] = true +			entry.names[ip] = true +			entry.time = os.time() +			entry.expires = nil +			entry.source = "xban:importer_minetest" +			table.insert(entry.record, { +				source = entry.source, +				reason = entry.reason, +				time = entry.time, +				expires = nil, +			}) +		end +	end +	f:close() +	return true +end diff --git a/xban2/importers/v1.lua b/xban2/importers/v1.lua new file mode 100644 index 0000000..a3722c7 --- /dev/null +++ b/xban2/importers/v1.lua @@ -0,0 +1,33 @@ + +function xban.importers.v1() +	local f, e = io.open(minetest.get_worldpath().."/players.iplist") +	if not f then +		return false, "Unable to open `players.iplist': "..e +	end +	for line in f:lines() do +		local list = line:split("|") +		if #list >= 2 then +			local banned = (list[1]:sub(1, 1) == "!") +			local entry +			entry = xban.find_entry(list[1], true) +			entry.banned = banned +			for _, name in ipairs(list) do +				entry.names[name] = true +			end +			if banned then +				entry.reason = "Banned in `players.iplist'" +				entry.time = os.time() +				entry.expires = nil +				entry.source = "xban:importer_v1" +				table.insert(entry.record, { +					source = entry.source, +					reason = entry.reason, +					time = entry.time, +					expires = nil, +				}) +			end +		end +	end +	f:close() +	return true +end diff --git a/xban2/importers/v2.lua b/xban2/importers/v2.lua new file mode 100644 index 0000000..fd29966 --- /dev/null +++ b/xban2/importers/v2.lua @@ -0,0 +1,35 @@ + +function xban.importers.v2() +	return pcall(function() +		local f, e = io.open(minetest.get_worldpath().."/players.iplist.v2") +		if not f then +			error("Unable to open `players.iplist.v2': "..e) +		end +		local text = f:read("*a") +		f:close() +		local db = minetest.deserialize(text) +		for _, e in ipairs(db) do +			for name in pairs(e.names) do +				local entry = xban.find_entry(name, true) +				if entry.source ~= "xban:importer_v2" then +					for nm in pairs(e.names) do +						entry.names[nm] = true +					end +					if e.banned then +						entry.banned = true +						entry.reason = e.banned +						entry.source = "xban:importer_v2" +						entry.time = e.time +						entry.expires = e.expires +						table.insert(entry.record, { +							source = entry.source, +							reason = entry.reason, +							time = entry.time, +							expires = entry.expires, +						}) +					end +				end +			end +		end +	end) +end diff --git a/xban2/init.lua b/xban2/init.lua new file mode 100644 index 0000000..81818a8 --- /dev/null +++ b/xban2/init.lua @@ -0,0 +1,331 @@ + +xban = { MP = minetest.get_modpath(minetest.get_current_modname()) } + +dofile(xban.MP.."/serialize.lua") + +local db = { } +local tempbans = { } + +local DEF_SAVE_INTERVAL = 300 -- 5 minutes +local DEF_DB_FILENAME = minetest.get_worldpath().."/xban.db" + +local DB_FILENAME = minetest.setting_get("xban.db_filename") +local SAVE_INTERVAL = tonumber( +  minetest.setting_get("xban.db_save_interval")) or DEF_SAVE_INTERVAL + +if (not DB_FILENAME) or (DB_FILENAME == "") then +	DB_FILENAME = DEF_DB_FILENAME +end + +local function make_logger(level) +	return function(text, ...) +		minetest.log(level, "[xban] "..text:format(...)) +	end +end + +local ACTION = make_logger("action") +local INFO = make_logger("info") +local WARNING = make_logger("warning") +local ERROR = make_logger("error") + +local unit_to_secs = { +	s = 1, m = 60, h = 3600, +	D = 86400, W = 604800, M = 2592000, Y = 31104000, +	[""] = 1, +} + +local function parse_time(t) --> secs +	local secs = 0 +	for num, unit in t:gmatch("(%d+)([smhDWMY]?)") do +		secs = secs + (tonumber(num) * (unit_to_secs[unit] or 1)) +	end +	return secs +end + +function xban.find_entry(player, create) --> entry, index +	for index, e in ipairs(db) do +		for name in pairs(e.names) do +			if name == player then +				return e, index +			end +		end +	end +	if create then +		print(("Created new entry for `%s'"):format(player)) +		local e = { +			names = { [player]=true }, +			banned = false, +			record = { }, +		} +		table.insert(db, e) +		return e, #db +	end +	return nil +end + +function xban.get_info(player) --> ip_name_list, banned, last_record +	local e = xban.find_entry(player) +	if not e then +		return nil, "No such entry" +	end +	return e.names, e.banned, e.record[#e.record] +end + +function xban.ban_player(player, source, expires, reason) --> bool, err +	local e = xban.find_entry(player, true) +	local rec = { +		source = source, +		time = os.time(), +		expires = expires, +		reason = reason, +	} +	table.insert(e.record, rec) +	e.names[player] = true +	local pl = minetest.get_player_by_name(player) +	if pl then +		local ip = minetest.get_player_ip(player) +		if ip then +			e.names[ip] = true +		end +		e.last_pos = pl:getpos() +	end +	e.reason = reason +	e.time = rec.time +	e.expires = expires +	e.banned = true +	local msg +	local date = (expires and os.date("%c", expires) +	  or "the end of time") +	if expires then +		table.insert(tempbans, e) +		msg = ("Banned: Expires: %s, Reason: %s"):format(date, reason) +	else +		msg = ("Banned: Reason: %s"):format(reason) +	end +	for nm in pairs(e.names) do +		minetest.kick_player(nm, msg) +	end +	ACTION("%s bans %s until %s for reason: %s", source, player, +	  date, reason) +	ACTION("Banned Names/IPs: %s", table.concat(e.names, ", ")) +	return true +end + +function xban.unban_player(player, source) --> bool, err +	local e = xban.find_entry(player) +	if not e then +		return nil, "No such entry" +	end +	local rec = { +		source = source, +		time = os.time(), +		reason = "Unbanned", +	} +	table.insert(e.record, rec) +	e.banned = false +	e.reason = nil +	e.expires = nil +	e.time = nil +	ACTION("%s unbans %s", source, player) +	ACTION("Unbanned Names/IPs: %s", table.concat(e.names, ", ")) +	return true +end + +function xban.get_record(player) +	local e = xban.find_entry(player) +	if not e then +		return nil, ("No entry for `%s'"):format(player) +	elseif (not e.record) or (#e.record == 0) then +		return nil, ("`%s' has no ban records"):format(player) +	end +	local record = { } +	for _, rec in ipairs(e.record) do +		local msg = rec.reason or "No reason given." +		if rec.expires then +			msg = msg..(", Expires: %s"):format(os.date("%c", e.expires)) +		end +		if rec.source then +			msg = msg..", Source: "..rec.source +		end +		table.insert(record, ("[%s]: %s"):format(os.date("%c", e.time), msg)) +	end +	local last_pos +	if e.last_pos then +		last_pos = ("User was last seen at %s"):format( +		  minetest.pos_to_string(e.last_pos)) +	end +	return record, last_pos +end + +minetest.register_on_prejoinplayer(function(name, ip) +	local e = xban.find_entry(name) or xban.find_entry(ip) +	if not e then return end +	if e.banned then +		local date = (e.expires and os.date("%c", e.expires) +		  or "the end of time") +		return ("Banned: Expires: %s, Reason: %s"):format( +		  date, e.reason) +	end +end) + +minetest.register_on_joinplayer(function(player) +	local name = player:get_player_name() +	local e = xban.find_entry(name) +	local ip = minetest.get_player_ip(name) +	if not e then +		if ip then +			e = xban.find_entry(ip, true) +		else +			return +		end +	end +	e.names[name] = true +	if ip then +		e.names[ip] = true +	end +	e.last_seen = os.time() +end) + +minetest.register_chatcommand("xban", { +	description = "XBan a player", +	params = "<player> <reason>", +	privs = { ban=true }, +	func = function(name, params) +		local plname, reason = params:match("(%S+)%s+(.+)") +		if not (plname and reason) then +			return false, "Usage: /xban <player> <reason>" +		end +		xban.ban_player(plname, name, nil, reason) +		return true, ("Banned %s."):format(plname) +	end, +}) + +minetest.register_chatcommand("xtempban", { +	description = "XBan a player temporarily", +	params = "<player> <time> <reason>", +	privs = { ban=true }, +	func = function(name, params) +		local plname, time, reason = params:match("(%S+)%s+(%S+)%s+(.+)") +		if not (plname and time and reason) then +			return false, "Usage: /xtempban <player> <time> <reason>" +		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 +		xban.ban_player(plname, name, expires, reason) +		return true, ("Banned %s until %s."):format(plname, os.date("%c", expires)) +	end, +}) + +minetest.register_chatcommand("xunban", { +	description = "XUnBan a player", +	params = "<player_or_ip>", +	privs = { ban=true }, +	func = function(name, params) +		local plname = params:match("%S+") +		if not plname then +			minetest.chat_send_player(name, +			  "Usage: /xunban <player_or_ip>") +			return +		end +		local ok, e = xban.unban_player(plname, name) +		return ok, ok and ("Unbanned %s."):format(plname) or e +	end, +}) + +minetest.register_chatcommand("xban_record", { +	description = "Show the ban records of a player", +	params = "<player_or_ip>", +	privs = { ban=true }, +	func = function(name, params) +		local plname = params:match("%S+") +		if not plname then +			return false, "Usage: /xban_record <player_or_ip>" +		end +		local record, last_pos = xban.get_record(plname) +		if not record then +			local err = last_pos +			minetest.chat_send_player(name, "[xban] "..err) +			return +		end +		for _, e in ipairs(record) do +			minetest.chat_send_player(name, "[xban] "..e) +		end +		if last_pos then +			minetest.chat_send_player(name, "[xban] "..last_pos) +		end +		return true, "Record listed." +	end, +}) + +local function check_temp_bans() +	minetest.after(60, check_temp_bans) +	local to_rm = { } +	local now = os.time() +	for i, e in ipairs(tempbans) do +		if e.expires and (e.expires <= now) then +			table.insert(to_rm, i) +			e.banned = false +			e.expires = nil +			e.reason = nil +			e.time = nil +		end +	end +	for _, i in ipairs(to_rm) do +		table.remove(tempbans, i) +	end +end + +local function save_db() +	minetest.after(SAVE_INTERVAL, save_db) +	local f, e = io.open(DB_FILENAME, "wt") +	db.timestamp = os.time() +	if f then +		local ok, err = f:write(xban.serialize(db)) +		if not ok then +			WARNING("Unable to save database: %s", err) +		end +	else +		WARNING("Unable to save database: %s", e) +	end +	if f then f:close() end +	return +end + +local function load_db() +	local f, e = io.open(DB_FILENAME, "rt") +	if not f then +		WARNING("Unable to load database: %s", e) +		return +	end +	local cont = f:read("*a") +	if not cont then +		WARNING("Unable to load database: %s", "Read failed") +		return +	end +	local t = minetest.deserialize(cont) +	if not t then +		WARNING("Unable to load database: %s", +		  "Deserialization failed") +		return +	end +	db = t +	tempbans = { } +	for _, entry in ipairs(db) do +		if entry.banned and entry.expires then +			table.insert(tempbans, entry) +		end +	end +end + +minetest.register_on_shutdown(save_db) +minetest.after(SAVE_INTERVAL, save_db) +load_db() +xban.db = db + +minetest.after(1, check_temp_bans) + +dofile(xban.MP.."/dbimport.lua") +dofile(xban.MP.."/gui.lua") diff --git a/xban2/serialize.lua b/xban2/serialize.lua new file mode 100644 index 0000000..c159141 --- /dev/null +++ b/xban2/serialize.lua @@ -0,0 +1,31 @@ + +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 + +function xban.serialize(t) +	return "return {\n"..my_serialize_2(t, 1).."\n}" +end  | 
