├── 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 | [![Build status](https://github.com/shivajiva101/sauth/workflows/Check%20&%20Release/badge.svg)](https://github.com/shivajiva101/sauth/actions) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | --------------------------------------------------------------------------------