diff options
| author | Diego Martinez <kaeza@users.sf.net> | 2014-05-19 23:23:43 -0300 | 
|---|---|---|
| committer | Diego Martinez <kaeza@users.sf.net> | 2014-05-19 23:23:43 -0300 | 
| commit | 8695476bebaea7469ea1aaf4c13ec8e020353d39 (patch) | |
| tree | 4c5be6e71a9926881b8a932c9a7f5a3f1a9e3751 | |
First commit.
| -rw-r--r-- | doc/API.md | 32 | ||||
| -rw-r--r-- | doc/dbformat.txt | 45 | ||||
| -rw-r--r-- | init.lua | 271 | ||||
| -rw-r--r-- | serialize.lua | 31 | ||||
| -rw-r--r-- | shutil.lua | 42 | 
5 files changed, 421 insertions, 0 deletions
| diff --git a/doc/API.md b/doc/API.md new file mode 100644 index 0000000..bee7c42 --- /dev/null +++ b/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/doc/dbformat.txt b/doc/dbformat.txt new file mode 100644 index 0000000..71b25a5 --- /dev/null +++ b/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/init.lua b/init.lua new file mode 100644 index 0000000..ef6143e --- /dev/null +++ b/init.lua @@ -0,0 +1,271 @@ + +xban = { } + +local MP = minetest.get_modpath(minetest.get_current_modname()) + +dofile(MP.."/serialize.lua") +dofile(MP.."/shutil.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 +		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 +	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 + +minetest.register_on_prejoinplayer(function(name, ip) +	local e = xban.find_entry(name) or xban.find_entry(ip, true) +	e.names[name] = true +	e.names[ip] = true +	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_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 +			minetest.chat_send_player(name, +			  "Usage: /xban <player> <reason>") +			return +		end +		xban.ban_player(plname, name, nil, reason) +		minetest.chat_send_player(name, +		  ("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 +			minetest.chat_send_player(name, +			  "Usage: /xtempban <player> <time> <reason>") +			return +		end +		time = parse_time(time) +		if time < 60 then +			minetest.chat_send_player(name, +			  "You must ban for at least 60 seconds.") +			return +		end +		local expires = os.time() + time +		xban.ban_player(plname, name, expires, reason) +		minetest.chat_send_player(name, +		  ("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) +		minetest.chat_send_player(name, +		  ("Unbanned %s."):format(plname)) +	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 ok +	local f, e = io.open(DB_FILENAME, "wt") +	db.timestamp = os.time() +	if f then +		ok, e = f:write(xban.serialize(db)) +		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 +	cont, e = f:read("*a") +	if not cont then +		WARNING("Unable to load database: %s", e) +		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 + +-- Backup database +shutil.copy_file(DB_FILENAME, +  DB_FILENAME.."."..os.date("%Y%m%d%H%M%S")) + +minetest.register_on_shutdown(save_db) +minetest.after(SAVE_INTERVAL, save_db) +load_db() +xban.db = db diff --git a/serialize.lua b/serialize.lua new file mode 100644 index 0000000..d9177d9 --- /dev/null +++ b/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 = ("  "):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 diff --git a/shutil.lua b/shutil.lua new file mode 100644 index 0000000..e8ca269 --- /dev/null +++ b/shutil.lua @@ -0,0 +1,42 @@ + +shutil = { } + +function shutil.copy_file(src, dst) +	local e, sf, df, cont, ok +	sf, e = io.open(src, "rb") +	if not sf then +		return nil, "Error opening input: "..(e or "") +	end +	df, e = io.open(dst, "wb") +	if not df then +		sf:close() +		return nil, "Error opening output: "..(e or "") +	end +	cont, e = sf:read("*a") +	if not cont then +		sf:close() +		df:close() +		return nil, "Error reading input: "..(e or "") +	end +	ok, e = df:write(cont) +	if not ok then +		sf:close() +		df:close() +		return nil, "Error writing output: "..(e or "") +	end +	sf:close() +	df:close() +	return true +end + +function shutil.move_file(src, dst) +	local ok, e = shutil.copy_file(src, dst) +	if not ok then +		return nil, "Copy failed: "..(e or "") +	end +	ok, e = os.remove(src) +	if not ok then +		return nil, "Remove failed: "..(e or "") +	end +	return true +end | 
