├── mod.conf
├── .luacheckrc
├── .github
└── workflows
│ └── check-release.yml
├── .gitignore
├── tools
├── instructions.txt
└── auth-convert.py
├── LICENSE
├── README.md
└── init.lua
/mod.conf:
--------------------------------------------------------------------------------
1 | name = sauth
2 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | unused_args = false
2 | allow_defined_top = true
3 |
4 | read_globals = {
5 | "minetest",
6 | string = {fields = {"split"}},
7 | table = {fields = {"copy", "getn"}},
8 | "vector", "default",
9 | }
10 |
--------------------------------------------------------------------------------
/.github/workflows/check-release.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 | name: Check & Release
3 | jobs:
4 | lint:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@master
8 | - name: lint
9 | uses: Roang-zero1/factorio-mod-luacheck@master
10 | with:
11 | luacheckrc_url: https://raw.githubusercontent.com/shivajiva101/sauth/0.5/.luacheckrc
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Lua sources
2 | luac.out
3 |
4 | # luarocks build files
5 | *.src.rock
6 | *.zip
7 | *.tar.gz
8 |
9 | # Object files
10 | *.o
11 | *.os
12 | *.ko
13 | *.obj
14 | *.elf
15 |
16 | # Precompiled Headers
17 | *.gch
18 | *.pch
19 |
20 | # Libraries
21 | *.lib
22 | *.a
23 | *.la
24 | *.lo
25 | *.def
26 | *.exp
27 |
28 | # Shared objects (inc. Windows DLLs)
29 | *.dll
30 | *.so
31 | *.so.*
32 | *.dylib
33 |
34 | # Executables
35 | *.exe
36 | *.out
37 | *.app
38 | *.i*86
39 | *.x86_64
40 | *.hex
41 |
42 |
--------------------------------------------------------------------------------
/tools/instructions.txt:
--------------------------------------------------------------------------------
1 | Copy the sauth.sqlite file into this folder and run the python script as follows:
2 |
3 | python3 auth-convert.py
4 |
5 | The script will create a new database file named out-auth.sqlite. How fast is it?
6 | In testing it processed a db with 305,000 records in ~10 seconds.
7 | Rename this file to sauth.sqlite to use with sauth v2.0 OR if you
8 | want to migrate your data for use with the builtin auth handler
9 | rename it auth.sqlite.
10 |
11 | Remember to backup your original file before replacing it with the converted db!
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 shivajiva101@hotmail.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sauth v2.0
2 | [](https://github.com/shivajiva101/sauth/actions)
3 | [](https://opensource.org/licenses/MIT)
4 |
5 | IMPORTANT: Version 2 changes the DB schema inline with the core auth schema. The existing sauth.sqlite will be converted so make a backup! ONLY USE with minetest version 0.5 or greater.
6 |
7 | Sauth is an alternative sqlite3 auth handler for minetest. Capable of handling large player databases and provides mitigation of auth entry request load by use of memory caching. Fine tune your servers auth memory load by caching clients who play regularly to reduce join event load on the server, resulting in a better playability experience on servers with many player accounts.
8 |
9 | Requires:
10 |
11 | * lsqlite3 lua library. (http://lua.sqlite.org/)
12 | * SQLite3 (https://www.sqlite.org/) OR your favourite SQL management app (for importing sql files)
13 |
14 | I suggest you use luarocks (https://luarocks.org/) to install lsqlite3.
15 |
16 | sudo apt install luarocks
17 | luarocks install lsqlite3
18 |
19 | Your server should always run mods in secure mode, you must add sauth to the list of trusted mods in minetest.conf for example:
20 |
21 | secure.trusted_mods = irc,sauth
22 |
23 | You can and should use your existing auth db, make a copy of auth.sqlite renaming it to sauth.sqlite BEFORE starting the server.
24 |
25 | To enable the mod for singleplayer add:
26 |
27 | sauth.enable_singleplayer = true
28 |
29 | to minetest.conf before starting the server.
30 |
31 | Caching comes at the expense of memory consumption. During server startup sauth initialises the cache with up to 500 players who logged in to the server in the last 24 hours before the last player to login prior to shutdown. You can manage the cache by adding these settings to minetest.conf and modifying the values otherwise the mod will use the hard coded defaults.
32 |
33 | sauth.cache_max = 500 -- default maximum number of memory cached entries on startup
34 | sauth.cache_ttl = 86400 -- default seconds deducted from last login
35 |
36 | Uninstalling
37 |
38 | If/when you want to remove sauth just delete the mod and minetest will default back to using the internal auth handler on the same db, it's as simple as that!
39 |
--------------------------------------------------------------------------------
/tools/auth-convert.py:
--------------------------------------------------------------------------------
1 | #!/bin/env python3
2 |
3 | # convert v1.1 sauth.sqlite file to builtin auth schema
4 |
5 | import sys, sqlite3, os
6 |
7 | name = "sauth.sqlite"
8 |
9 | if not os.path.isfile(name):
10 | print("You must run this script in a folder where sauth.sqlite exists!")
11 | sys.exit(1)
12 |
13 | try:
14 | db = sqlite3.connect(name)
15 | except:
16 | print("You must use a database file!")
17 | sys.exit(1)
18 |
19 | cur = db.cursor()
20 |
21 | # check input file table format
22 | has_id = False
23 | has_name = False
24 | has_password = False
25 | has_privileges = False
26 | has_last_login = False
27 |
28 | for row in cur.execute("PRAGMA table_info('auth');"):
29 | has_id |= row[1] == "id"
30 | has_name |= row[1] == "name"
31 | has_password |= row[1] == "password"
32 | has_privileges |= row[1] == "privileges"
33 | has_last_login |= row[1] == "last_login"
34 |
35 | if has_id and has_name and has_password and has_privileges and has_last_login:
36 | out_name = "out-auth.sqlite"
37 | else:
38 | print("db file does not have the right table format!")
39 | sys.exit(1)
40 |
41 | if os.path.isfile(out_name):
42 | print(f"Error: remove or rename {out_name} first!")
43 | sys.exit(1)
44 |
45 | print(f"Creating {out_name} file...")
46 |
47 | try:
48 | db2 = sqlite3.connect(out_name)
49 | except:
50 | print(f"Unable to open {out_name} for writing")
51 | sys.exit(1)
52 |
53 | cur2 = db2.cursor()
54 |
55 | print("Creating db tables...")
56 |
57 | # create auth format db file
58 | cur2.execute("CREATE TABLE `auth` (`id` INTEGER PRIMARY KEY AUTOINCREMENT,`name` VARCHAR(32) UNIQUE,`password` VARCHAR(512),`last_login` INTEGER);")
59 | cur2.execute("CREATE TABLE `user_privileges` (`id` INTEGER,`privilege` VARCHAR(32),PRIMARY KEY (id, privilege)CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES auth (id) ON DELETE CASCADE);")
60 | cur2.execute("CREATE TABLE `tmp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT,`name` VARCHAR(32) UNIQUE ON CONFLICT REPLACE,`password` VARCHAR(512),`last_login` INTEGER);")
61 | db2.commit()
62 |
63 | print("Copying entries...")
64 | ctr = 0
65 | last_id = 0
66 | privs = []
67 |
68 | # fetch auth table data and insert it into tmp table
69 | cur.execute("SELECT id, name, password, last_login FROM auth;")
70 | result = cur.fetchall()
71 |
72 | cur2.executemany("INSERT INTO tmp VALUES (?, ?, ?, ?);", result)
73 | db2.commit()
74 |
75 | # fetch tmp table data and insert it into auth table
76 | cur2.execute("SELECT id, name, password, last_login FROM tmp;")
77 | result = cur2.fetchall()
78 |
79 | cur2.executemany("INSERT INTO auth VALUES (?, ?, ?, ?);", result)
80 | db2.commit()
81 |
82 | # drop tmp table
83 | cur2.execute("DROP TABLE tmp;")
84 | db2.commit()
85 |
86 | # split privs from auth table into user_privileges table
87 | for row in cur.execute("SELECT id, privileges FROM auth;"):
88 | ctr += 1
89 | last_id = row[0]
90 |
91 | for priv in row[1].replace(" ", "").split(","):
92 | privs.append((row[0], priv))
93 |
94 | cur2.executemany("INSERT INTO user_privileges VALUES (?, ?);", privs)
95 | db2.commit()
96 |
97 | print(f"{str(ctr)} records written, last id = {str(last_id)}")
98 |
99 | # end
100 | cur2.execute("VACUUM;")
101 | db2.commit()
102 | db.close()
103 | db2.close()
104 |
--------------------------------------------------------------------------------
/init.lua:
--------------------------------------------------------------------------------
1 | -- sqlite3 auth handler mod with memory caching for minetest voxel game
2 | -- by shivajiva101@hotmail.com
3 |
4 | -- Expose handler functions
5 | sauth = {}
6 | local cache = {}
7 | local MN = minetest.get_current_modname()
8 | local WP = minetest.get_worldpath()
9 | local ie = minetest.request_insecure_environment()
10 | local owner_privs_cached = false
11 |
12 | if not ie then
13 | error("insecure environment inaccessible"..
14 | " - make sure this mod has been added to minetest.conf!")
15 | end
16 |
17 | -- read mt conf file settings
18 | local max_cache_records = tonumber(minetest.settings:get(MN .. '.cache_max')) or 500
19 | local ttl = tonumber(minetest.settings:get(MN..'.cache_ttl')) or 86400 -- defaults to 24 hours
20 | local owner = minetest.settings:get("name")
21 |
22 | -- localise library for db access
23 | local _sql = ie.require("lsqlite3")
24 |
25 | -- Prevent use of this db instance. If you want to run mods that
26 | -- don't secure this global make sure they load AFTER this mod!
27 | if sqlite3 then sqlite3 = nil end
28 |
29 | local singleplayer = minetest.is_singleplayer()
30 |
31 | -- Use conf setting to determine handler for singleplayer
32 | if not minetest.settings:get_bool(MN .. '.enable_singleplayer')
33 | and singleplayer then
34 | minetest.log("info", "singleplayer game using builtin auth handler")
35 | return
36 | end
37 |
38 | -- check if sauth.sqlite is present
39 | local file1_exists = io.open(WP.."/sauth.sqlite", "r") ~= nil
40 | local file2_exists = io.open(WP.."/auth.sqlite", "r") ~= nil
41 | local update = false
42 |
43 | if file1_exists then
44 | local ok, msg
45 | update = true
46 | -- Fix file names
47 | if file2_exists then
48 | ok, msg = ie.os.rename(WP.."/auth.sqlite", WP.."/auth.sqlite.bak")
49 | if not ok then minetest.log('error', msg) end
50 | end
51 | ok, msg = ie.os.rename(WP.."/sauth.sqlite", WP.."/auth.sqlite")
52 | if not ok then minetest.log('error', msg) end
53 |
54 | end
55 |
56 | local db = _sql.open(WP.."/auth.sqlite") -- connection
57 |
58 | --- Apply statements against the current database
59 | --- wrapping db:exec for error reporting
60 | ---@param stmt string
61 | ---@return boolean
62 | ---@return string error message
63 | local function db_exec(stmt)
64 | if db:exec(stmt) ~= _sql.OK then
65 | minetest.log("info", "Sqlite ERROR: "..db:errmsg())
66 | return false, db:errmsg()
67 | end
68 | return true
69 | end
70 |
71 | -- Alter table name, create new tables & copy data over
72 | -- parsing player privileges to new format and clean up
73 | local function updater()
74 |
75 | minetest.log('action', "Updating sauth db...")
76 |
77 | local stmt = "ALTER TABLE auth RENAME TO auth_tmp;"
78 | db_exec(stmt)
79 |
80 | stmt = ([[
81 | CREATE TABLE IF NOT EXISTS auth (
82 | id INTEGER PRIMARY KEY AUTOINCREMENT,
83 | name VARCHAR(32) UNIQUE,
84 | password VARCHAR(512),
85 | last_login INTEGER);
86 | CREATE TABLE IF NOT EXISTS user_privileges (
87 | id INTEGER,
88 | privilege VARCHAR(32),
89 | PRIMARY KEY (id, privilege) CONSTRAINT fk_id FOREIGN KEY (id)
90 | REFERENCES auth (id) ON DELETE CASCADE);
91 | ]])
92 | db_exec(stmt)
93 |
94 | stmt = ([[
95 | DELETE FROM auth_tmp WHERE id IN (SELECT id FROM auth_tmp GROUP BY name HAVING COUNT(*)>1);
96 | INSERT INTO auth SELECT id, name, password, last_login FROM auth_tmp;
97 | ]])
98 | db_exec(stmt)
99 |
100 | local data = {}
101 | stmt = "SELECT id, privileges FROM auth_tmp;"
102 | for row in db:nrows(stmt) do
103 | data[#data+1] = row
104 | end
105 |
106 | local sb = {}
107 | local hdr = true
108 | local ftr = false
109 |
110 | for i = 1, #data do
111 | if hdr then
112 | sb[#sb+1] = "PRAGMA foreign_keys = OFF;"
113 | sb[#sb+1] = "BEGIN TRANSACTION;"
114 | hdr = false
115 | end
116 | if ftr then
117 | sb[#sb+1] = "COMMIT;"
118 | sb[#sb+1] = "PRAGMA foreign_keys = ON;"
119 | stmt = table.concat(sb, "\n")
120 | db_exec(stmt)
121 | sb = {}
122 | ftr = false
123 | hdr = true
124 | end
125 | local id = data[i].id
126 | local privs = minetest.string_to_privs(data[i].privileges)
127 | for priv, _ in pairs(privs) do
128 | if priv then
129 | sb[#sb+1] = ("INSERT INTO user_privileges (id, privilege) VALUES (%i, '%s');"):format(id, priv)
130 | end
131 | end
132 | if #sb > 1000 then
133 | ftr = true
134 | end
135 | end
136 | -- check for zero sb length!
137 | if #sb > 0 then
138 | sb[#sb+1] = "DROP TABLE auth_tmp;"
139 | sb[#sb+1] = "DROP TABLE _s;"
140 | sb[#sb+1] = "COMMIT;"
141 | sb[#sb+1] = "PRAGMA foreign_keys = ON;"
142 | sb[#sb+1] = "VACUUM;"
143 | else
144 | sb[#sb+1] = "VACUUM;"
145 | end
146 | stmt = table.concat(sb, "\n")
147 | db_exec(stmt)
148 | minetest.log('action', "sauth db was converted and renamed to minetest auth.sqlite!")
149 | end
150 | -- Update database check
151 | if update then updater() end
152 |
153 | -- Cache handling
154 | local cap = 0
155 |
156 | --- Create cache when mod loads
157 | local function create_cache()
158 | local q = "SELECT max(last_login) AS result FROM auth;"
159 | local it, state = db:nrows(q)
160 | local last = it(state)
161 | if last and last.result then
162 | last = last.result - ttl
163 | q = ([[SELECT * FROM auth WHERE last_login > %s LIMIT %s;
164 | ]]):format(last, max_cache_records)
165 | for row in db:nrows(q) do
166 | cache[row.name] = {
167 | id = row.id,
168 | password = row.password,
169 | privileges = {},
170 | last_login = row.last_login
171 | }
172 | cap = cap + 1
173 | end
174 | for k,v in pairs(cache) do
175 | local r = {}
176 | q = ("SELECT * FROM user_privileges WHERE id = %i;"):format(v.id)
177 | for row in db:nrows(q) do
178 | r[row.privilege] = true
179 | end
180 | cache[k].privileges = r
181 | end
182 | end
183 | minetest.log("action", "[sauth] caching " .. cap .. " records.")
184 | end
185 |
186 | --- Remove oldest entry in the cache
187 | local function trim_cache()
188 | if cap < max_cache_records then return end
189 | local entry = os.time()
190 | local name
191 | for k, v in pairs(cache) do
192 | if v.last_login < entry then
193 | entry = v.last_login
194 | name = k
195 | end
196 | end
197 | cache[name] = nil
198 | cap = cap - 1
199 | end
200 |
201 | --- Sanitises the string param
202 | ---@param str string
203 | ---@return sanitised string
204 | local function sanitize(str)
205 | str = str:gsub("%'", '')
206 | return str:gsub('[%c%s]', '')
207 | end
208 |
209 | -- Define db tables
210 | local create_db = [[
211 | CREATE TABLE IF NOT EXISTS auth (
212 | id INTEGER PRIMARY KEY AUTOINCREMENT,
213 | name VARCHAR(32) UNIQUE,
214 | password VARCHAR(512),
215 | last_login INTEGER);
216 | CREATE TABLE IF NOT EXISTS user_privileges (
217 | id INTEGER,
218 | privilege VARCHAR(32),
219 | PRIMARY KEY (id, privilege) CONSTRAINT fk_id FOREIGN KEY (id)
220 | REFERENCES auth (id) ON DELETE CASCADE);
221 | ]]
222 | db_exec(create_db)
223 |
224 | create_cache()
225 |
226 |
227 | --[[
228 | ###########################
229 | ### Database: Queries ###
230 | ###########################
231 | ]]
232 |
233 | --- Get auth table record for name
234 | ---@param name string
235 | ---@return keypair table
236 | local function get_auth_record(name)
237 | local query = ([[
238 | SELECT * FROM auth WHERE name = '%s' LIMIT 1;
239 | ]]):format(name)
240 | local it, state = db:nrows(query)
241 | local row = it(state)
242 | return row
243 | end
244 |
245 | --- Get privileges from user_privileges table for id
246 | ---@param id integer
247 | ---@return keypairs table or nil
248 | local function get_privs(id)
249 | local q = ([[
250 | SELECT * FROM user_privileges WHERE id = %i;
251 | ]]):format(id)
252 | local r = {}
253 | for row in db:nrows(q) do
254 | r[row.privilege] = true
255 | end
256 | return r
257 | end
258 |
259 | --- Get id from player name
260 | ---@param name string
261 | ---@return id integer or nil
262 | local function get_id(name)
263 | local q = ("SELECT * FROM auth WHERE name = '%s';"):format(name)
264 | local it, state = db:nrows(q)
265 | local row = it(state)
266 | return row.id
267 | end
268 |
269 | --- Check db for matching name
270 | ---@param name string
271 | ---@return table or nil
272 | local function check_name(name)
273 | local query = ([[
274 | SELECT DISTINCT name
275 | FROM auth
276 | WHERE LOWER(name) = LOWER('%s') LIMIT 1;
277 | ]]):format(name)
278 | local it, state = db:nrows(query)
279 | local row = it(state)
280 | return row
281 | end
282 |
283 | --- Search for records where the name is like param string
284 | ---@param name string
285 | ---@return table ipairs
286 | --- Uses sql LIKE %name% to pattern match any
287 | --- string that contains name
288 | local function search(name)
289 | local r,q = {}
290 | q = "SELECT name FROM auth WHERE name LIKE '%"..name.."%';"
291 | for row in db:nrows(q) do
292 | r[#r+1] = row.name
293 | end
294 | return r
295 | end
296 |
297 | --- Get pairs table of names in the database
298 | ---@return table
299 | local function get_names()
300 | local r,q = {}
301 | q = "SELECT name FROM auth;"
302 | for row in db:nrows(q) do
303 | r[row.name] = true
304 | end
305 | return r
306 | end
307 |
308 |
309 | --[[
310 | ###########################
311 | ### Database: Inserts ###
312 | ###########################
313 | ]]
314 |
315 | --- Add auth record to database
316 | ---@param name string
317 | ---@param password string
318 | ---@param privs pairs table
319 | ---@param last_login integer
320 | ---@return boolean
321 | ---@return string error message
322 | local function add_player_record(name, password, privs, last_login)
323 | local stmt = ([[
324 | INSERT INTO auth (
325 | name,
326 | password,
327 | last_login
328 | ) VALUES ('%s','%s', %i)
329 | ]]):format(name, password, last_login)
330 | local r, e = db_exec(stmt)
331 | if r then
332 | -- add privileges
333 | local str = {}
334 | local id = db:last_insert_rowid()
335 | for k,v in pairs(privs) do
336 | str[#str + 1] = ([[
337 | INSERT INTO user_privileges (
338 | id,
339 | privilege
340 | ) VALUES (%i, '%s');
341 | ]]):format(id, k)
342 | end
343 | return db_exec(table.concat(str, "\n"))
344 | else
345 | return r, e
346 | end
347 | end
348 |
349 |
350 | --[[
351 | ###########################
352 | ### Database: Updates ###
353 | ###########################
354 | ]]
355 |
356 | --- Update last login for a player
357 | ---@param name string
358 | ---@param timestamp integer
359 | ---@return boolean
360 | ---@return string error message
361 | local function update_auth_login(name, timestamp)
362 | local stmt = ([[
363 | UPDATE auth SET last_login = %i WHERE name = '%s'
364 | ]]):format(timestamp, name)
365 | return db_exec(stmt)
366 | end
367 |
368 | --- Update password for a player
369 | ---@param name string
370 | ---@param password string
371 | ---@return boolean
372 | ---@return string error message
373 | local function update_password(name, password)
374 | local stmt = ([[
375 | UPDATE auth SET password = '%s' WHERE name = '%s'
376 | ]]):format(password,name)
377 | return db_exec(stmt)
378 | end
379 |
380 | --- Update privileges for a player
381 | ---@param name string
382 | ---@param privs pair table
383 | ---@return boolean
384 | ---@return string error message
385 | local function update_privileges(name, privs)
386 | local id = get_id(name)
387 | local stmt = ([[
388 | DELETE FROM user_privileges WHERE id = %i;
389 | ]]):format(id)
390 | local r, e = db_exec(stmt)
391 | if r == true then
392 | local str = {}
393 | for k,v in pairs(privs) do
394 | str[#str + 1] = ([[
395 | INSERT INTO user_privileges (
396 | id,
397 | privilege
398 | ) VALUES (%i, '%s');
399 | ]]):format(id, k)
400 | end
401 | return db_exec(table.concat(str, "\n"))
402 | else
403 | return r, e
404 | end
405 | end
406 |
407 |
408 | --[[
409 | #############################
410 | ### Database: Deletions ###
411 | #############################
412 | ]]
413 |
414 | --- Delete a players auth record from the database
415 | ---@param name string
416 | ---@return boolean
417 | ---@return string error message
418 | local function del_record(name)
419 | local stmt = ([[
420 | DELETE FROM auth WHERE name = '%s';
421 | ]]):format(name)
422 | return db_exec(stmt)
423 | end
424 |
425 |
426 | --[[
427 | ###################
428 | ### Functions ###
429 | ###################
430 | ]]
431 |
432 | --- Returns a complete player record
433 | ---@param name string
434 | ---@return keypair table or nil
435 | local function get_player_record(name)
436 | local r = get_auth_record(name)
437 | if r then r.privileges = get_privs(r.id) end
438 | return r
439 | end
440 |
441 | --- Get Player db record
442 | ---@param name string
443 | ---@return keypair table
444 | local function get_record(name)
445 | -- Prioritise cache
446 | if cache[name] then return cache[name] end
447 | return get_player_record(name)
448 | end
449 |
450 | --- Update last login for a player
451 | ---@param name string
452 | ---@param timestamp integer
453 | ---@return boolean
454 | ---@return string error message
455 | local function update_login(name)
456 | local ts = os.time()
457 | if cache[name] then
458 | cache[name].last_login = ts
459 | else
460 | sauth.auth_handler.get_auth(name)
461 | end
462 | return update_auth_login(name, ts)
463 | end
464 |
465 |
466 | --[[
467 | ######################
468 | ### Auth Handler ###
469 | ######################
470 | ]]
471 |
472 | sauth.auth_handler = {
473 |
474 | --- Return auth record entry with privileges as a pair table
475 | --- Prioritises cached data over repeated db searches
476 | ---@param name string
477 | ---@param add_to_cache boolean optional - default is true
478 | ---@return keypairs table
479 | get_auth = function(name, add_to_cache)
480 |
481 | -- Check param
482 | assert(type(name) == 'string')
483 | name = sanitize(name)
484 |
485 | -- if an auth record is cached ensure
486 | -- the owner is granted admin privs
487 | if cache[name] then
488 | if not owner_privs_cached and name == owner then
489 | -- grant admin privs
490 | for priv, def in pairs(minetest.registered_privileges) do
491 | if def.give_to_admin then
492 | cache[name].privileges[priv] = true
493 | end
494 | end
495 | owner_privs_cached = true
496 | end
497 | return cache[name]
498 | end
499 |
500 | -- Assert caching on missing param
501 | add_to_cache = add_to_cache or true
502 |
503 | -- Check db for matching record
504 | local auth_entry = get_player_record(name)
505 |
506 | -- Unknown name check
507 | if not auth_entry then return nil end
508 |
509 | -- Make a copy of the players privilege table.
510 | local privileges ={}
511 | for priv, _ in pairs(auth_entry.privileges) do
512 | privileges[priv] = true
513 | end
514 |
515 | -- If singleplayer, grant privileges marked give_to_singleplayer
516 | if minetest.is_singleplayer() then
517 | for priv, def in pairs(minetest.registered_privileges) do
518 | if def.give_to_singleplayer then
519 | privileges[priv] = true
520 | end
521 | end
522 |
523 | -- Grant owner all privileges
524 | elseif name == owner then
525 | for priv, def in pairs(minetest.registered_privileges) do
526 | if def.give_to_admin then
527 | privileges[priv] = true
528 | end
529 | end
530 | end
531 |
532 | -- Construct record
533 | local record = {
534 | password = auth_entry.password,
535 | privileges = privileges,
536 | last_login = tonumber(auth_entry.last_login)}
537 |
538 | -- Conditionally retrieves records without caching
539 | -- by passing false as the second param
540 | if add_to_cache then
541 | cache[name] = record
542 | cap = cap + 1
543 | end
544 |
545 | return record
546 | end,
547 |
548 | --- Create a new auth entry
549 | ---@param name string
550 | ---@param password string
551 | ---@return boolean
552 | create_auth = function(name, password)
553 | assert(type(name) == 'string')
554 | assert(type(password) == 'string')
555 | minetest.log('info', "[sauth] authentification handler adding player '"..name.."'")
556 | local privs = minetest.string_to_privs(minetest.settings:get("default_privs"))
557 | local res, err = add_player_record(name,password,privs,-1)
558 | if res then
559 | cache[name] = {
560 | password = password,
561 | privileges = privs,
562 | last_login = -1 -- defer
563 | }
564 | end
565 | return res, err
566 | end,
567 |
568 | --- Delete an auth entry
569 | ---@param name string
570 | ---@return boolean
571 | delete_auth = function(name)
572 | assert(type(name) == 'string')
573 | local record = get_record(name)
574 | local res = false
575 | if record then
576 | minetest.log('info', "[sauth] authentification handler deleting player '"..name.."'")
577 | res = del_record(name)
578 | if res then
579 | cache[name] = nil
580 | end
581 | end
582 | return res
583 | end,
584 |
585 | --- Set password for an auth record
586 | ---@param name string
587 | ---@param password string
588 | ---@return boolean
589 | set_password = function(name, password)
590 | assert(type(name) == 'string')
591 | assert(type(password) == 'string')
592 | -- get player record
593 | if get_record(name) == nil then
594 | sauth.auth_handler.create_auth(name, password)
595 | else
596 | update_password(name, password)
597 | if cache[name] then cache[name].password = password end
598 | end
599 | return true
600 | end,
601 |
602 | --- Set privileges for an auth record
603 | ---@param name string
604 | ---@param privileges keypairs table
605 | ---@return boolean
606 | set_privileges = function(name, privileges)
607 | assert(type(name) == 'string')
608 | assert(type(privileges) == 'table')
609 | local auth_entry = sauth.auth_handler.get_auth(name)
610 | if not auth_entry then
611 | auth_entry = sauth.auth_handler.create_auth(name,
612 | minetest.get_password_hash(name,
613 | minetest.settings:get("default_password")))
614 | end
615 | -- Run grant callbacks
616 | for priv, _ in pairs(privileges) do
617 | if not auth_entry.privileges[priv] then
618 | minetest.run_priv_callbacks(name, priv, nil, "grant")
619 | end
620 | end
621 | -- Run revoke callbacks
622 | for priv, _ in pairs(auth_entry.privileges) do
623 | if not privileges[priv] then
624 | minetest.run_priv_callbacks(name, priv, nil, "revoke")
625 | end
626 | end
627 | -- Ensure owner has ability to grant
628 | if name == owner then privileges.privs = true end
629 | -- Update record
630 | update_privileges(name, privileges)
631 | if cache[name] then cache[name].privileges = privileges end
632 | minetest.notify_authentication_modified(name)
633 | return true
634 | end,
635 |
636 | --- Reload database
637 | ---@param return boolean
638 | reload = function()
639 | cache = {}
640 | create_cache()
641 | return true
642 | end,
643 |
644 | --- Records the last login timestamp
645 | ---@param name string
646 | ---@return boolean
647 | ---@return string error message
648 | record_login = function(name)
649 | assert(type(name) == 'string')
650 | return update_login(name)
651 | end,
652 |
653 | --- Searches for names like param
654 | ---@param name string
655 | ---@return table ipairs
656 | name_search = function(name)
657 | assert(type(name) == 'string')
658 | return search(name)
659 | end,
660 |
661 | --- Return an iterator function for the auth table names
662 | ---@return function iterator
663 | iterate = function()
664 | local names = get_names()
665 | return pairs(names)
666 | end,
667 | }
668 |
669 |
670 | --[[
671 | ########################
672 | ### Register hooks ###
673 | ########################
674 | ]]
675 |
676 | -- Register auth handler
677 | minetest.register_authentication_handler(sauth.auth_handler)
678 |
679 | -- Log event as minetest registers silently
680 | minetest.log('action', "[sauth] registered as the authentication handler!")
681 |
682 | minetest.register_on_prejoinplayer(function(name, ip)
683 | local r = get_record(name)
684 | if r ~= nil then return end
685 | -- Check name isn't registered
686 | local chk = check_name(name)
687 | if chk then
688 | return ("\nCannot create new player called '%s'. "..
689 | "Another account called '%s' is already registered.\n"..
690 | "Please check the spelling if it's your account "..
691 | "or use a different name."):format(name, chk.name)
692 | end
693 | end)
694 |
695 | minetest.register_on_joinplayer(function(player)
696 | local name = player:get_player_name()
697 | local r = get_record(name)
698 | if r ~= nil then sauth.auth_handler.record_login(name) end
699 | trim_cache()
700 | end)
701 |
702 | minetest.register_on_shutdown(function()
703 | db:close()
704 | end)
705 |
--------------------------------------------------------------------------------