· 9 years ago · Mar 09, 2017, 01:28 AM
1local modname = minetest.get_current_modname()
2local modpath = minetest.get_modpath(modname)
3
4local thismod = {}
5_G[modname] = thismod
6
7local singleplayer = minetest.is_singleplayer() -- Caching is OK since you can't open a game to
8-- multiplayer unless you restart it.
9if not minetest.setting_get(modname .. '.enable_singleplayer') and singleplayer then
10 core.log('action', modname .. ": Not adding auth handler because of singleplayer game")
11 return
12end
13
14local function setoverlay(tab, orig)
15 local mt = getmetatable(tab) or {}
16 mt.__index = function (tab, key)
17 if rawget(tab, key) ~= nil then
18 return rawget(tab, key)
19 else
20 return orig[key]
21 end
22 end
23 setmetatable(tab, mt)
24end
25
26local function string_splitdots(s)
27 local temp = {}
28 local index = 0
29 local last_index = string.len(s)
30 while true do
31 local i, e = string.find(s, '%.', index)
32 if i and e then
33 local next_index = e + 1
34 local word_bound = i - 1
35 table.insert(temp, string.sub(s, index, word_bound))
36 index = next_index
37 else
38 if index > 0 and index <= last_index then
39 table.insert(temp, string.sub(s, index, last_index))
40 elseif index == 0 then
41 temp = nil
42 end
43 break
44 end
45 end
46 return temp
47end
48
49local ie = minetest.request_insecure_environment()
50if not ie then error("Failed to get insecure environment - make sure this mod is trusted!") end
51local mysql
52do -- MySQL module loading
53 local env = {
54 require = function (module)
55 if module == 'mysql_h' then
56 return ie.dofile(modpath .. '/mysql/mysql_h.lua')
57 else
58 return ie.require(module)
59 end
60 end
61 }
62 setoverlay(env, _G)
63 local fn, msg = ie.loadfile(modpath .. '/mysql/mysql.lua')
64 if not fn then error(msg) end
65 setfenv(fn, env)
66 local status
67 status, mysql = pcall(fn, {})
68 if not status then
69 error(modname .. ' failed to load MySQL FFI interface: ' .. mysql)
70 end
71end
72
73do
74 local get
75 do
76 get = function (name) return minetest.setting_get(modname .. '.' .. name) end
77 local cfgfile = get('cfgfile')
78 if type(cfgfile) == 'string' and cfgfile ~= '' then
79 local file = io.open(cfgfile, 'rb')
80 if not file then
81 error(modname .. ' failed to load specified config file at ' .. cfgfile)
82 end
83 local cfg, msg = minetest.deserialize(file:read('*a'))
84 file:close()
85 if not cfg then
86 error(modname .. ' failed to parse specified config file at ' .. cfgfile .. ': ' .. msg)
87 end
88 get = function (name)
89 if type(name) ~= 'string' or name == '' then
90 return nil
91 end
92 local parts = string_splitdots(name)
93 if not parts then
94 return cfg[name]
95 end
96 local tbl = cfg[parts[1]]
97 for n = 2, #parts do
98 if tbl == nil then
99 return nil
100 end
101 tbl = tbl[parts[n]]
102 end
103 return tbl
104 end
105 end
106 end
107
108 local conn, dbname
109 do
110 -- MySQL API backend
111 mysql.config(get('db.api'))
112
113 local connopts = get('db.connopts')
114 if (get('db.db') == nil) and (type(connopts) == 'table' and connopts.db == nil) then
115 error(modname .. ": missing database name parameter")
116 end
117 if type(connopts) ~= 'table' then
118 connopts = {}
119 -- Traditional connection parameters
120 connopts.host, connopts.user, connopts.port, connopts.pass, connopts.db =
121 get('db.host') or 'localhost', get('db.user'), get('db.port'), get('db.pass'), get('db.db')
122 end
123 connopts.charset = 'utf8'
124 connopts.options = connopts.options or {}
125 connopts.options.MYSQL_OPT_RECONNECT = true
126 conn = mysql.connect(connopts)
127 dbname = connopts.db
128 thismod.conn = conn
129
130 -- LuaPower's MySQL interface throws an error when the connection fails, no need to check if
131 -- it succeeded.
132
133 -- Ensure UTF-8 is in use.
134 -- If you use another encoding, kill yourself (unless it's UTF-32).
135 conn:query("SET NAMES 'utf8'")
136 conn:query("SET CHARACTER SET utf8")
137 conn:query("SET character_set_results = 'utf8', character_set_client = 'utf8'," ..
138 "character_set_connection = 'utf8', character_set_database = 'utf8'," ..
139 "character_set_server = 'utf8'")
140
141 local set = function(setting, val) conn:query('SET ' .. setting .. '=' .. val) end
142 pcall(set, 'wait_timeout', 3600)
143 pcall(set, 'autocommit', 1)
144 pcall(set, 'max_allowed_packet', 67108864)
145 end
146
147 local tables = {}
148 do -- Tables and schema settings
149 local t_auths = get('db.tables.auths')
150 if type(t_auths) == 'table' then
151 tables.auths = t_auths
152 else
153 tables.auths = {}
154 tables.auths.name = get('db.tables.auths.name')
155 tables.auths.schema = {}
156 local S = tables.auths.schema
157 S.userid = get('db.tables.auths.schema.userid')
158 S.username = get('db.tables.auths.schema.username')
159 S.password = get('db.tables.auths.schema.password')
160 S.privs = get('db.tables.auths.schema.privs')
161 S.lastlogin = get('db.tables.auths.schema.lastlogin')
162 S.userid_type = get('db.tables.auths.schema.userid_type')
163 S.username_type = get('db.tables.auths.schema.username_type')
164 S.password_type = get('db.tables.auths.schema.password_type')
165 S.privs_type = get('db.tables.auths.schema.privs_type')
166 S.lastlogin_type = get('db.tables.auths.schema.lastlogin_type')
167 end
168
169 do -- Default values
170 tables.auths.name = tables.auths.name or 'auths'
171 tables.auths.schema = tables.auths.schema or {}
172 local S = tables.auths.schema
173 S.userid = S.userid or 'userid'
174 S.username = S.username or 'username'
175 S.password = S.password or 'password'
176 S.privs = S.privs or 'privs'
177 S.lastlogin = S.lastlogin or 'lastlogin'
178
179 S.userid_type = S.userid_type or 'INT'
180 S.username_type = S.username_type or 'VARCHAR(32)'
181 S.password_type = S.password_type or 'VARCHAR(512)'
182 S.privs_type = S.privs_type or 'VARCHAR(512)'
183 S.lastlogin_type = S.lastlogin_type or 'BIGINT'
184 -- Note lastlogin doesn't use the TIMESTAMP type, which is 32-bit and therefore
185 -- subject to the year 2038 problem.
186 end
187 end
188
189 local auth_table_created
190 do -- Auth table existence check and setup
191 conn:query("SHOW TABLES LIKE '" .. tables.auths.name .. "'")
192 local res = conn:store_result()
193 local exists = (res:row_count() ~= 0)
194 res:free()
195 if not exists then
196 -- Auth table doesn't exist, create it
197 local S = tables.auths.schema
198 conn:query('CREATE TABLE ' .. tables.auths.name .. ' (' ..
199 S.userid .. ' ' .. S.userid_type .. ' NOT NULL AUTO_INCREMENT,' ..
200 S.username .. ' ' .. S.username_type .. ' NOT NULL,' ..
201 S.password .. ' ' .. S.password_type .. ' NOT NULL,' ..
202 S.privs .. ' ' .. S.privs_type .. ' NOT NULL,' ..
203 S.lastlogin .. ' ' .. S.lastlogin_type .. ',' ..
204 'PRIMARY KEY (' .. S.userid .. '),' ..
205 'UNIQUE (' .. S.username .. ')' ..
206 ')')
207 minetest.log('action', modname .. " created table '" .. dbname .. "." .. tables.auths.name ..
208 "'")
209 auth_table_created = true
210 end
211 end
212
213 local S = tables.auths.schema
214 local get_auth_stmt = conn:prepare('SELECT ' .. S.password .. ',' .. S.privs .. ',' ..
215 S.lastlogin .. ' FROM ' .. tables.auths.name .. ' WHERE ' .. S.username .. '=?')
216 thismod.get_auth_stmt = get_auth_stmt
217 local get_auth_params = get_auth_stmt:bind_params({S.username_type})
218 thismod.get_auth_params = get_auth_params
219 local get_auth_results = get_auth_stmt:bind_result({S.password_type, S.privs_type,
220 S.lastlogin_type})
221 thismod.get_auth_results = get_auth_results
222
223 local create_auth_stmt = conn:prepare('INSERT INTO ' .. tables.auths.name .. '(' .. S.username ..
224 ',' .. S.password .. ',' .. S.privs .. ',' .. S.lastlogin .. ') VALUES (?,?,?,?)')
225 thismod.create_auth_stmt = create_auth_stmt
226 local create_auth_params = create_auth_stmt:bind_params({S.username_type, S.password_type,
227 S.privs_type, S.lastlogin_type})
228 thismod.create_auth_params = create_auth_params
229
230 local delete_auth_stmt = conn:prepare('DELETE FROM ' .. tables.auths.name .. ' WHERE ' ..
231 S.username .. '=?')
232 thismod.delete_auth_stmt = delete_auth_stmt
233 local delete_auth_params = delete_auth_stmt:bind_params({S.username_type})
234 thismod.delete_auth_params = delete_auth_params
235
236 local set_password_stmt = conn:prepare('UPDATE ' .. tables.auths.name .. ' SET ' .. S.password ..
237 '=? WHERE ' .. S.username .. '=?')
238 thismod.set_password_stmt = set_password_stmt
239 local set_password_params = set_password_stmt:bind_params({S.password_type, S.username_type})
240 thismod.set_password_params = set_password_params
241
242 local set_privileges_stmt = conn:prepare('UPDATE ' .. tables.auths.name .. ' SET ' .. S.privs ..
243 '=? WHERE ' .. S.username .. '=?')
244 thismod.set_privileges_stmt = set_privileges_stmt
245 local set_privileges_params = set_privileges_stmt:bind_params({S.privs_type, S.username_type})
246 thismod.set_privileges_params = set_privileges_params
247
248 local record_login_stmt = conn:prepare('UPDATE ' .. tables.auths.name .. ' SET ' ..
249 S.lastlogin .. '=? WHERE ' .. S.username .. '=?')
250 thismod.record_login_stmt = record_login_stmt
251 local record_login_params = record_login_stmt:bind_params({S.lastlogin_type, S.username_type})
252 thismod.record_login_params = record_login_params
253
254 local enumerate_auths_query = 'SELECT ' .. S.username .. ',' .. S.password .. ',' .. S.privs ..
255 ',' .. S.lastlogin .. ' FROM ' .. tables.auths.name
256 thismod.enumerate_auths_query = enumerate_auths_query
257
258 if auth_table_created and get('import_auth_txt_on_table_create') ~= 'false' then
259 if not thismod.import_auth_txt then
260 dofile(modpath .. '/auth_txt_import.lua')
261 end
262 thismod.import_auth_txt()
263 end
264
265 thismod.auth_handler = {
266 get_auth = function(name)
267 assert(type(name) == 'string')
268 get_auth_params:set(1, name)
269 local success, msg = pcall(get_auth_stmt.exec, get_auth_stmt)
270 if not success then
271 minetest.log('error', modname .. ": get_auth(" .. name .. ") failed: " .. msg)
272 return nil
273 end
274 get_auth_stmt:store_result()
275 if not get_auth_stmt:fetch() then
276 -- No such auth row exists
277 return nil
278 end
279 while get_auth_stmt:fetch() do
280 minetest.log('warning', modname .. ": get_auth(" .. name .. "): multiples lines were" ..
281 " returned")
282 end
283 local password, privs_str, lastlogin = get_auth_results:get(1), get_auth_results:get(2),
284 get_auth_results:get(3)
285 local admin = (name == minetest.setting_get("name"))
286 local privs
287 if singleplayer or admin then
288 privs = {}
289 -- If admin, grant all privs, if singleplayer, grant all privs w/ give_to_singleplayer
290 for priv, def in pairs(core.registered_privileges) do
291 if (singleplayer and def.give_to_singleplayer) or admin then
292 privs[priv] = true
293 end
294 end
295 if admin and not thismod.admin_get_auth_called then
296 thismod.admin_get_auth_called = true
297 thismod.auth_handler.set_privileges(name, privs)
298 end
299 else
300 privs = minetest.string_to_privs(privs_str)
301 end
302 return {
303 password = password,
304 privileges = privs,
305 last_login = tonumber(lastlogin)
306 }
307 end,
308 create_auth = function(name, password, reason)
309 assert(type(name) == 'string')
310 assert(type(password) == 'string')
311 minetest.log('info', modname .. " creating player '"..name.."'" .. (reason or ""))
312 create_auth_params:set(1, name)
313 create_auth_params:set(2, password)
314 create_auth_params:set(3, minetest.setting_get("default_privs"))
315 create_auth_params:set(4, math.floor(os.time()))
316 local success, msg = pcall(create_auth_stmt.exec, create_auth_stmt)
317 if not success then
318 minetest.log('error', modname .. ": create_auth(" .. name .. ") failed: " .. msg)
319 return false
320 end
321 if create_auth_stmt:affected_rows() ~= 1 then
322 minetest.log('error', modname .. ": create_auth(" .. name .. ") failed: affected row" ..
323 " count is " .. create_auth_stmt:affected_rows() .. ", expected 1")
324 return false
325 end
326 return true
327 end,
328 delete_auth = function(name)
329 assert(type(name) == 'string')
330 minetest.log('info', modname .. " deleting player '"..name.."'")
331 delete_auth_params:set(1, name)
332 local success, msg = pcall(delete_auth_stmt.exec, delete_auth_stmt)
333 if not success then
334 minetest.log('error', modname .. ": delete_auth(" .. name .. ") failed: " .. msg)
335 return false
336 end
337 if delete_auth_stmt:affected_rows() ~= 1 then
338 minetest.log('error', modname .. ": delete_auth(" .. name .. ") failed: affected row" ..
339 " count is " .. delete_auth_stmt:affected_rows() .. ", expected 1")
340 return false
341 end
342 return true
343 end,
344 set_password = function(name, password)
345 assert(type(name) == 'string')
346 assert(type(password) == 'string')
347 if not thismod.auth_handler.get_auth(name) then
348 return thismod.auth_handler.create_auth(name, password, " because set_password was requested")
349 else
350 minetest.log('info', modname .. " setting password of player '" .. name .. "'")
351 set_password_params:set(1, password)
352 set_password_params:set(2, name)
353 local success, msg = pcall(set_password_stmt.exec, set_password_stmt)
354 if not success then
355 minetest.log('error', modname .. ": set_password(" .. name .. ") failed: " .. msg)
356 return false
357 end
358 if set_password_stmt:affected_rows() ~= 1 then
359 minetest.log('error', modname .. ": set_password(" .. name .. ") failed: affected row" ..
360 " count is " .. set_password_stmt:affected_rows() .. ", expected 1")
361 return false
362 end
363 return true
364 end
365 end,
366 set_privileges = function(name, privileges)
367 assert(type(name) == 'string')
368 assert(type(privileges) == 'table')
369 set_privileges_params:set(1, minetest.privs_to_string(privileges))
370 set_privileges_params:set(2, name)
371 local success, msg = pcall(set_privileges_stmt.exec, set_privileges_stmt)
372 if not success then
373 minetest.log('error', modname .. ": set_privileges(" .. name .. ") failed: " .. msg)
374 return false
375 end
376 minetest.notify_authentication_modified(name)
377 if set_privileges_stmt:affected_rows() ~= 1 then
378 minetest.log('error', modname .. ": set_privileges(" .. name .. ") failed: affected row" ..
379 " count is " .. set_privileges_stmt:affected_rows() .. ", expected 1")
380 return false
381 end
382 return true
383 end,
384 reload = function()
385 return true
386 end,
387 record_login = function(name)
388 assert(type(name) == 'string')
389 record_login_params:set(1, math.floor(os.time()))
390 record_login_params:set(2, name)
391 local success, msg = pcall(record_login_stmt.exec, record_login_stmt)
392 if not success then
393 minetest.log('error', modname .. ": record_login(" .. name .. ") failed: " .. msg)
394 return false
395 end
396 if record_login_stmt:affected_rows() ~= 1 then
397 minetest.log('error', modname .. ": record_login(" .. name .. ") failed: affected row" ..
398 " count is " .. record_login_stmt:affected_rows() .. ", expected 1")
399 return false
400 end
401 return true
402 end,
403 enumerate_auths = function()
404 conn:query(enumerate_auths_query)
405 local res = conn:store_result()
406 return function()
407 local row = res:fetch('n')
408 if not row then
409 return nil
410 end
411 local username, password, privs_str, lastlogin = unpack(row)
412 return username, {
413 password = password,
414 privileges = minetest.string_to_privs(privs_str),
415 last_login = tonumber(lastlogin)
416 }
417 end
418 end
419 }
420end
421
422minetest.register_authentication_handler(thismod.auth_handler)
423minetest.log('action', modname .. ": Registered auth handler")
424
425local function ping()
426 if thismod.conn then
427 if not thismod.conn:ping() then
428 minetest.log('error', modname .. ": failed to ping database")
429 end
430 end
431 minetest.after(1800, ping)
432end
433minetest.after(10, ping)
434
435minetest.register_on_shutdown(function()
436 if thismod.conn then
437 thismod.get_auth_stmt:free_result()
438 thismod.conn:close()
439 thismod.conn = nil
440 end
441end)