init.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. xban = xban or {}
  2. xban.db = xban.db or {}
  3. xban.tempbans = xban.tempbans or {}
  4. xban.MP = minetest.get_modpath("xban2")
  5. -- Reloadable.
  6. dofile(xban.MP.."/serialize.lua")
  7. local DEF_SAVE_INTERVAL = 300 -- 5 minutes
  8. local DEF_DB_FILENAME = minetest.get_worldpath().."/xban.db"
  9. local DB_FILENAME = minetest.settings:get("xban.db_filename")
  10. local SAVE_INTERVAL = tonumber(
  11. minetest.settings:get("xban.db_save_interval")) or DEF_SAVE_INTERVAL
  12. if (not DB_FILENAME) or (DB_FILENAME == "") then
  13. DB_FILENAME = DEF_DB_FILENAME
  14. end
  15. local function make_logger(level)
  16. return function(text, ...)
  17. minetest.log(level, "[xban] "..text:format(...))
  18. end
  19. end
  20. local ACTION = make_logger("action")
  21. local WARNING = make_logger("warning")
  22. local unit_to_secs = {
  23. s = 1, m = 60, h = 3600,
  24. D = 86400, W = 604800, M = 2592000, Y = 31104000,
  25. [""] = 1,
  26. }
  27. local function parse_time(t) --> secs
  28. local secs = 0
  29. for num, unit in t:gmatch("(%d+)([smhDWMY]?)") do
  30. secs = secs + (tonumber(num) * (unit_to_secs[unit] or 1))
  31. end
  32. return secs
  33. end
  34. function xban.find_entry(player, create) --> entry, index
  35. for index, e in ipairs(xban.db) do
  36. for name in pairs(e.names) do
  37. if name == player then
  38. return e, index
  39. end
  40. end
  41. end
  42. if create then
  43. ACTION("Created new entry for `%s'", player)
  44. local e = {
  45. names = { [player]=true },
  46. banned = false,
  47. record = { },
  48. last_pos = { },
  49. last_seen = { },
  50. }
  51. table.insert(xban.db, e)
  52. return e, #xban.db
  53. end
  54. return nil
  55. end
  56. function xban.get_info(player) --> ip_name_list, banned, last_record
  57. local e = xban.find_entry(player)
  58. if not e then
  59. return nil, "No such entry."
  60. end
  61. return e.names, e.banned, e.record[#e.record]
  62. end
  63. function xban.ban_player(player, source, expires, reason) --> bool, err
  64. if xban.get_whitelist(player) then
  65. return nil, "Player is whitelisted; remove from whitelist first!"
  66. end
  67. local e = xban.find_entry(player, true)
  68. if e.banned then
  69. return nil, "Player already banned."
  70. end
  71. if minetest.check_player_privs(player, {server=true}) then
  72. return nil, "Administrators cannot be banned!"
  73. end
  74. local rec = {
  75. source = source,
  76. time = os.time(), -- Date of ban.
  77. expires = expires,
  78. reason = reason,
  79. }
  80. table.insert(e.record, rec)
  81. e.names[player] = true
  82. local pl = minetest.get_player_by_name(player)
  83. if pl then
  84. local ip = minetest.get_player_ip(player)
  85. if ip then
  86. e.names[ip] = true
  87. end
  88. e.last_pos[player] = pl:get_pos()
  89. end
  90. e.reason = reason
  91. e.time = rec.time
  92. e.expires = expires
  93. e.banned = true
  94. local msg
  95. local date = (expires and os.date("%c", expires)
  96. or "The End of Time")
  97. if expires then
  98. table.insert(xban.tempbans, e)
  99. msg = ("Banned: Expires: %s, Reason: %s"):format(date, reason)
  100. else
  101. msg = ("Banned: Reason: %s"):format(reason)
  102. end
  103. for nm in pairs(e.names) do
  104. minetest.kick_player(nm, msg)
  105. end
  106. ACTION("%s bans %s until %s for reason: %s", source, player,
  107. date, reason)
  108. ACTION("Banned Names/IPs: %s", table.concat(e.names, ", "))
  109. return true
  110. end
  111. function xban.unban_player(player, source) --> bool, err
  112. local e = xban.find_entry(player)
  113. if not e then
  114. return nil, "No such entry."
  115. end
  116. if not e.banned then
  117. return nil, "Player not banned."
  118. end
  119. local rec = {
  120. source = source,
  121. time = os.time(),
  122. reason = "Unbanned",
  123. }
  124. table.insert(e.record, rec)
  125. e.banned = false
  126. e.reason = nil
  127. e.expires = nil
  128. e.time = nil
  129. ACTION("%s unbans %s", source, player)
  130. ACTION("Unbanned Names/IPs: %s", table.concat(e.names, ", "))
  131. return true
  132. end
  133. function xban.get_whitelist(name_or_ip)
  134. return xban.db.whitelist and xban.db.whitelist[name_or_ip]
  135. end
  136. function xban.remove_whitelist(name_or_ip)
  137. if xban.db.whitelist then
  138. xban.db.whitelist[name_or_ip] = nil
  139. end
  140. end
  141. function xban.add_whitelist(name_or_ip, source)
  142. local wl = xban.db.whitelist
  143. if not wl then
  144. wl = { }
  145. xban.db.whitelist = wl
  146. end
  147. wl[name_or_ip] = {
  148. source=source,
  149. }
  150. return true
  151. end
  152. function xban.get_record(player)
  153. local e = xban.find_entry(player)
  154. if not e then
  155. return nil, ("No entry for <%s>."):format(rename.gpn(player))
  156. elseif (not e.record) or (#e.record == 0) then
  157. return nil, ("<%s> has no ban records."):format(rename.gpn(player))
  158. end
  159. local record = { }
  160. for _, rec in ipairs(e.record) do
  161. local msg = rec.reason and ("Reason: '" .. rec.reason .. "'") or "No reason given"
  162. if rec.expires then
  163. msg = msg..(", Expires: %s"):format(os.date("%c", e.expires))
  164. end
  165. if rec.source then
  166. msg = msg..", Source: '"..rec.source.."'"
  167. end
  168. table.insert(record, ("[%s]: %s."):format(os.date("%c", e.time), msg))
  169. end
  170. local last_pos
  171. if e.last_pos and e.last_pos[player] then
  172. last_pos = ("User was last seen at %s."):format(
  173. rc.pos_to_namestr(vector.round(e.last_pos[player])))
  174. end
  175. return record, last_pos
  176. end
  177. function xban.on_prejoinplayer(name, ip)
  178. if minetest.check_player_privs(name, {server=true}) then
  179. return
  180. end
  181. local wl = xban.db.whitelist or { }
  182. if wl[name] or wl[ip] then return end
  183. local e = xban.find_entry(name) or xban.find_entry(ip)
  184. if not e then return end
  185. if e.banned then
  186. local date = (e.expires and os.date("%c", e.expires) or "The End of Time")
  187. return ("\nBanned!\nExpires: %s\nReason: %s"):format(date, e.reason)
  188. end
  189. end
  190. function xban.on_joinplayer(player)
  191. local name = player:get_player_name()
  192. local e = xban.find_entry(name)
  193. local ip = minetest.get_player_ip(name)
  194. if not e then
  195. -- Don't create database entries for players who never registered.
  196. -- This keeps the database size limited to only those players who
  197. -- decided to play on the server for a while. Guests are not included.
  198. if ip and passport.player_registered(name) then
  199. e = xban.find_entry(ip, true)
  200. else
  201. return
  202. end
  203. end
  204. e.names[name] = true
  205. if ip then
  206. e.names[ip] = true
  207. end
  208. e.last_seen[name] = os.time()
  209. end
  210. function xban.on_leaveplayer(player, timeout)
  211. local pname = player:get_player_name()
  212. -- Don't record last_pos for temporary accounts.
  213. if not passport.player_registered(pname) then
  214. return
  215. end
  216. local e = xban.find_entry(pname)
  217. if e then
  218. e.last_pos[pname] = player:get_pos()
  219. end
  220. end
  221. function xban.chatcommand_ban(name, params)
  222. local plname, reason = params:match("^(%S+)%s+(.+)$")
  223. if not (plname and reason) then
  224. minetest.chat_send_player(name, "# Server: Usage: '/xban <player> <reason>'.")
  225. return false
  226. end
  227. reason = reason:trim()
  228. local ok, e = xban.ban_player(plname, rename.gpn(name), nil, reason)
  229. local msg = ("# Server: Banned <%s>."):format(rename.gpn(plname))
  230. if not ok then
  231. msg = "# Server: " .. e
  232. end
  233. minetest.chat_send_player(name, msg)
  234. return ok
  235. end
  236. function xban.chatcommand_tempban(name, params)
  237. local plname, time, reason = params:match("^(%S+)%s+(%S+)%s+(.+)$")
  238. if not (plname and time and reason) then
  239. minetest.chat_send_player(name, "# Server: Usage: '/xtempban <player> <time> <reason>'.")
  240. return false
  241. end
  242. time = parse_time(time)
  243. if time < 60 then
  244. minetest.chat_send_player(name, "# Server: You must ban for at least 60 seconds.")
  245. return false
  246. end
  247. reason = reason:trim()
  248. local expires = os.time() + time
  249. local ok, e = xban.ban_player(plname, rename.gpn(name), expires, reason)
  250. local msg = ("# Server: Banned <%s> until %s."):format(rename.gpn(plname), os.date("%c", expires))
  251. if not ok then
  252. msg = "# Server: " .. e
  253. end
  254. minetest.chat_send_player(name, msg)
  255. return ok
  256. end
  257. function xban.chatcommand_xunban(name, params)
  258. local plname = params:match("^%S+$")
  259. if not plname or plname == "" then
  260. minetest.chat_send_player(name, "# Server: Usage: '/xunban <player>|<ip>'.")
  261. return
  262. end
  263. local ok, e = xban.unban_player(plname, rename.gpn(name))
  264. local msg = ("# Server: Unbanned <%s>."):format(rename.gpn(plname))
  265. if not ok then
  266. msg = "# Server: " .. e
  267. end
  268. minetest.chat_send_player(name, msg)
  269. return ok
  270. end
  271. function xban.chatcommand_record(name, params)
  272. local plname = params:match("^%S+$")
  273. if not plname or plname == "" then
  274. minetest.chat_send_player(name, "# Server: Usage: '/xban_record <player>|<ip>'.")
  275. return false
  276. end
  277. local record, last_pos = xban.get_record(plname)
  278. if not record then
  279. local err = last_pos
  280. minetest.chat_send_player(name, "# Server: [xban]: "..err)
  281. return
  282. end
  283. for _, e in ipairs(record) do
  284. minetest.chat_send_player(name, "# Server: [xban]: "..e)
  285. end
  286. if last_pos then
  287. minetest.chat_send_player(name, "# Server: [xban]: "..last_pos)
  288. end
  289. minetest.chat_send_player(name, "# Server: [xban]: End of record.")
  290. return true
  291. end
  292. function xban.chatcommand_wl(name, params)
  293. local cmd, plname = params:match("^%s*(%S+)%s*(%S+)%s*$")
  294. if not cmd or cmd == "" then
  295. minetest.chat_send_player(name, "# Server: Usage: '/xban_wl (add|del|get) <name>|<ip>'.")
  296. return false
  297. end
  298. if not plname or plname == "" then
  299. minetest.chat_send_player(name, "# Server: Usage: '/xban_wl (add|del|get) <name>|<ip>'.")
  300. return false
  301. end
  302. if cmd == "add" then
  303. xban.add_whitelist(plname, rename.gpn(name))
  304. ACTION("%s adds %s to whitelist", name, plname)
  305. minetest.chat_send_player(name, "# Server: Added <"..rename.gpn(plname).."> to whitelist!")
  306. return true
  307. elseif cmd == "del" then
  308. xban.remove_whitelist(plname)
  309. ACTION("%s removes %s to whitelist", name, plname)
  310. minetest.chat_send_player(name, "# Server: Removed <"..rename.gpn(plname).."> from whitelist!")
  311. return true
  312. elseif cmd == "get" then
  313. local e = xban.get_whitelist(plname)
  314. if e then
  315. minetest.chat_send_player(name, "# Server: Source: "..(e.source and ("'"..e.source.."'") or "Unknown")..".")
  316. return true
  317. else
  318. minetest.chat_send_player(name, "# Server: No whitelist for <"..rename.gpn(plname)..">!")
  319. return true
  320. end
  321. end
  322. end
  323. function xban.check_temp_bans()
  324. minetest.after(60, function() xban.check_temp_bans() end)
  325. local to_rm = { }
  326. local now = os.time()
  327. for i, e in ipairs(xban.tempbans) do
  328. if e.expires and (e.expires <= now) then
  329. table.insert(to_rm, i)
  330. e.banned = false
  331. e.expires = nil
  332. e.reason = nil
  333. e.time = nil
  334. end
  335. end
  336. for _, i in ipairs(to_rm) do
  337. table.remove(xban.tempbans, i)
  338. end
  339. end
  340. function xban.save_db()
  341. minetest.after(SAVE_INTERVAL, function() xban.save_db() end)
  342. local f, e = io.open(DB_FILENAME, "wt")
  343. xban.db.timestamp = os.time()
  344. if f then
  345. local ok, err = f:write(xban.serialize(xban.db))
  346. if not ok then
  347. WARNING("Unable to save database: %s", err)
  348. end
  349. else
  350. WARNING("Unable to save database: %s", e)
  351. end
  352. if f then f:close() end
  353. return
  354. end
  355. function xban.load_db()
  356. local f, e = io.open(DB_FILENAME, "rt")
  357. if not f then
  358. WARNING("Unable to load database: %s", e)
  359. return
  360. end
  361. local cont = f:read("*a")
  362. if not cont then
  363. WARNING("Unable to load database: %s", "Read failed")
  364. return
  365. end
  366. local t, e2 = minetest.deserialize(cont)
  367. if not t then
  368. WARNING("Unable to load database: %s",
  369. "Deserialization failed: "..(e2 or "unknown error"))
  370. return
  371. end
  372. local function is_vector(data)
  373. local count = 0
  374. if type(data) == "table" then
  375. for k, v in pairs(data) do
  376. count = count + 1
  377. end
  378. end
  379. if count ~= 3 then
  380. return false
  381. end
  382. if type(data) == "table"
  383. and type(data.x) == "number"
  384. and type(data.y) == "number"
  385. and type(data.z) == "number"
  386. and count == 3 then
  387. return true
  388. end
  389. return false
  390. end
  391. -- Update DB format. Remove entries that are in an old, invalid format.
  392. for k, v in ipairs(t) do
  393. if type(v.last_seen) ~= "nil" and type(v.last_seen) ~= "table" then
  394. v.last_seen = nil
  395. end
  396. if type(v.last_pos) ~= "nil" and is_vector(v.last_pos) then
  397. v.last_pos = nil
  398. end
  399. -- Add empty tables for missing elements.
  400. if not v.last_pos then
  401. v.last_pos = {}
  402. end
  403. if not v.last_seen then
  404. v.last_seen = {}
  405. end
  406. end
  407. xban.db = t
  408. xban.tempbans = {}
  409. for _, entry in ipairs(xban.db) do
  410. if entry.banned and entry.expires then
  411. table.insert(xban.tempbans, entry)
  412. end
  413. end
  414. end
  415. if not xban.registered then
  416. minetest.register_on_joinplayer(function(...)
  417. return xban.on_joinplayer(...)
  418. end)
  419. minetest.register_on_prejoinplayer(function(...)
  420. return xban.on_prejoinplayer(...)
  421. end)
  422. minetest.register_on_leaveplayer(function(...)
  423. return xban.on_leaveplayer(...)
  424. end)
  425. minetest.register_chatcommand("xban_wl", {
  426. description = "Manages the XBan whitelist.",
  427. params = "(add|del|get) <name>|<ip>",
  428. privs = { ban=true },
  429. func = function(...)
  430. return xban.chatcommand_wl(...)
  431. end,
  432. })
  433. minetest.register_chatcommand("xban_record", {
  434. description = "Show the ban records of a player.",
  435. params = "<player>|<ip>",
  436. privs = { ban=true },
  437. func = function(...)
  438. return xban.chatcommand_record(...)
  439. end,
  440. })
  441. minetest.register_chatcommand("xunban", {
  442. description = "XUnban a player.",
  443. params = "<player>|<ip>",
  444. privs = { ban=true },
  445. func = function(...)
  446. return xban.chatcommand_xunban(...)
  447. end,
  448. })
  449. minetest.register_chatcommand("xtempban", {
  450. description = "XBan a player temporarily.",
  451. params = "<player> <time> <reason>",
  452. privs = { ban=true },
  453. func = function(...)
  454. return xban.chatcommand_tempban(...)
  455. end,
  456. })
  457. minetest.register_chatcommand("xban", {
  458. description = "XBan a player.",
  459. params = "<player> <reason>",
  460. privs = { ban=true },
  461. func = function(...)
  462. return xban.chatcommand_ban(...)
  463. end,
  464. })
  465. minetest.register_on_shutdown(function(...) return xban.save_db(...) end)
  466. minetest.after(SAVE_INTERVAL, function() xban.save_db() end)
  467. minetest.after(1, function() xban.check_temp_bans() end)
  468. xban.load_db()
  469. local c = "xban:core"
  470. local f = xban.MP .. "/init.lua"
  471. reload.register_file(c, f, false)
  472. xban.registered = true
  473. end
  474. -- Reloadable.
  475. dofile(xban.MP.."/dbimport.lua")
  476. dofile(xban.MP.."/gui.lua")