Last active
May 15, 2018 16:05
-
-
Save sorcerykid/8e567ba1b4b737e79c2469d4cbe93d6f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-------------------------------------------------------- | |
-- Minetest :: Auth Redux Mod v2.1 (auth_rx) | |
-- | |
-- See README.txt for licensing and release notes. | |
-- Copyright (c) 2017-2018, Leslie E. Krause | |
-- | |
-- ./games/just_test_tribute/mods/auth_rx/init.lua | |
-------------------------------------------------------- | |
---------------------------- | |
-- Transaction Op Codes | |
---------------------------- | |
local LOG_STARTED = 10 -- <timestamp> 0 | |
local LOG_CHECKED = 11 -- <timestamp> 1 | |
local LOG_STOPPED = 12 -- <timestamp> 2 | |
local TX_CREATE = 20 -- <timestamp> 105 <username> | |
local TX_DELETE = 21 -- <timestamp> 106 <username> | |
local TX_SET_PASSWORD = 40 -- <timestamp> 200 <username> <password> | |
local TX_SET_FILTERED_ADDRS = 41 -- <timestamp> 201 <username> <filrered_addrs> | |
local TX_SET_ASSIGNED_PRIVS = 42 -- <timestamp> 202 <username> <assigned_privs> | |
local TX_SESSION_OPENED = 50 -- <timestamp> 102 <username> | |
local TX_SESSION_CLOSED = 51 -- <timestamp> 103 <username> | |
local TX_ATTEMPT_SUCCESS = 30 -- <timestamp> 200 <username> | |
local TX_ATTEMPT_FAILURE = 31 -- <timestamp> 400 <username> | |
local auth_db | |
---------------------------- | |
-- Journal Class | |
---------------------------- | |
local Journal = function ( path ) | |
local file, err = io.open( path .. "/auth.dbx", "r+b" ) | |
local self = { } | |
local cursor = 0 | |
local rtime = 1.0 | |
if not file then | |
minetest.log( "error", "Cannot open journal file for writing!" ) | |
error( "Fatal exception in Journal( ), aborting." ) | |
end | |
-- Advance to the last set of noncommitted transactions (if any) | |
for line in file:lines( ) do | |
local fields = string.split( line, " ", true ) | |
if tonumber( fields[ 2 ] ) == LOG_STOPPED then | |
cursor = file:seek( ) | |
end | |
end | |
file:seek( "set", cursor ) | |
self.audit = function ( update_proc, commit_proc ) | |
-- Update the database with all noncommitted transactions | |
local meta = { } | |
for line in file:lines( ) do | |
local fields = string.split( line, " ", true ) | |
local optime = tonumber( fields[ 1 ] ) | |
local opcode = tonumber( fields[ 2 ] ) | |
update_proc( meta, optime, opcode, select( 3, unpack( fields ) ) ) | |
if opcode == LOG_CHECKED then | |
-- Perform the commit and reset the log, if successful | |
commit_proc( ) | |
file:seek( "set", cursor ) | |
file:write( optime .. " " .. LOG_STOPPED .. "\n" ) | |
end | |
cursor = file:seek( ) | |
end | |
end | |
self.start = function ( ) | |
self.optime = os.time( ) | |
file:seek( "end", 0 ) | |
file:write( self.optime .. " " .. LOG_STARTED .. "\n" ) | |
cursor = file:seek( ) | |
file:write( self.optime .. " " .. LOG_CHECKED .. "\n" ) | |
end | |
self.reset = function ( ) | |
file:seek( "set", cursor ) | |
file:write( self.optime .. " " .. LOG_STOPPED .. "\n" ) | |
self.optime = nil | |
end | |
self.record_raw = function ( opcode, ... ) | |
file:seek( "set", cursor ) | |
file:write( table.concat( { self.optime, opcode, unpack( arg ) }, " " ) .. "\n" ) | |
cursor = file:seek( ) | |
file:write( self.optime .. " " .. LOG_CHECKED .. "\n" ) | |
end | |
minetest.register_globalstep( function( dtime ) | |
rtime = rtime - dtime | |
if rtime <= 0.0 then | |
if self.optime then | |
-- touch file every 1.0 secs so we know if/when server crashes | |
self.optime = os.time( ) | |
file:seek( "set", cursor ) | |
file:write( self.optime .. " " .. LOG_CHECKED .. "\n" ) | |
end | |
rtime = 1.0 | |
end | |
end ) | |
return self | |
end | |
---------------------------- | |
-- AuthDatabase Class | |
---------------------------- | |
local AuthDatabase = function ( path ) | |
local data, size, users | |
local self = { } | |
local journal = Journal( path ) | |
-- Private methods | |
local find_phrase = function( source, phrase ) | |
-- sanitize search phrase and convert to regexp pattern | |
local sanitizer = | |
{ | |
["^"] = "%^"; | |
["$"] = "%$"; | |
["("] = "%("; | |
[")"] = "%)"; | |
["%"] = "%%"; | |
["."] = "%."; | |
["["] = ""; | |
["]"] = ""; | |
["*"] = "%w*"; | |
["+"] = "%w+"; | |
["-"] = "%-"; | |
["?"] = "%w"; | |
} | |
-- parens capture only first return value of gsub | |
return string.find( string.upper( source ), ( string.gsub( string.upper( phrase ), ".", sanitizer ) ) ) | |
end | |
local db_update = function( meta, optime, opcode, ... ) | |
local fields = arg | |
print( "db_update( )", optime, opcode ) | |
print( dump( meta ) ) | |
if opcode == TX_CREATE then | |
local rec = | |
{ | |
password = fields[ 2 ], | |
oldlogin = optime, | |
newlogin = optime, | |
filtered_addrs = { }, | |
assigned_privs = { }, | |
sessions_count = 0, | |
sessions_length = 0, | |
attempts_success = 0, | |
attempts_failure = 0 | |
} | |
data[ fields[ 1 ] ] = rec | |
elseif opcode == TX_DELETE then | |
data[ fields[ 1 ] ] = nil | |
elseif opcode == TX_SET_PASSWORD then | |
data[ fields[ 1 ] ].password = fields[ 2 ] | |
elseif opcode == TX_SET_FILTERED_ADDRS then | |
data[ fields[ 1 ] ].filered_addrs = string.split( fields[ 2 ], ",", true ) | |
elseif opcode == TX_SET_ASSIGNED_PRIVS then | |
data[ fields[ 1 ] ].assigned_privs = string.split( fields[ 2 ], ",", true ) | |
elseif opcode == TX_ATTEMPT_FAILURE then | |
data[ fields[ 1 ] ].attempts_failure = data[ fields[ 1 ] ].attempts_failure + 1 | |
elseif opcode == TX_ATTEMPT_SUCCESS then | |
data[ fields[ 1 ] ].newlogin = optime | |
data[ fields[ 1 ] ].attempts_success = data[ fields[ 1 ] ].attempts_success + 1 | |
elseif opcode == TX_SESSION_OPENED then | |
data[ fields[ 1 ] ].sessions_count = data[ fields[ 1 ] ].sessions_count + 1 | |
meta.users[ fields[ 1 ] ] = optime | |
elseif opcode == TX_SESSION_CLOSED then | |
data[ fields[ 1 ] ].sessions_length = data[ fields[ 1 ] ].sessions_length + ( optime - meta.users[ fields[ 1 ] ] ) | |
meta.users[ fields[ 1 ] ] = nil | |
elseif opcode == LOG_STARTED then | |
meta.users = { } | |
elseif opcode == LOG_CHECKED then | |
-- calculate leftover session lengths due to abnormal server termination | |
for u, t in pairs( meta.users ) do | |
data[ u ].sessions_length = data[ u ].sessions_length + ( optime - t ) | |
end | |
meta.users = nil | |
end | |
end | |
local db_reload = function ( ) | |
minetest.log( "action", "Reading authentication data from disk..." ) | |
local file, errmsg = io.open( path .. "/auth.db", "r+b" ) | |
if not file then | |
minetest.log( "error", "Cannot open " .. path .. "/auth.db for reading." ) | |
error( "Fatal exception in AuthDatabase:reload( ), aborting." ) | |
end | |
for line in file:lines( ) do | |
if line ~= "" then | |
local fields = string.split( line, ":", true ) | |
if #fields ~= 10 then | |
minetest.log( "error", "Invalid record in authentication database." ) | |
error( "Fatal exception in AuthDatabase:reload( ), aborting." ) | |
end | |
data[ fields[ 1 ] ] = { | |
password = fields[ 2 ], | |
oldlogin = tonumber( fields[ 3 ] ), | |
newlogin = tonumber( fields[ 4 ] ), | |
filtered_addrs = string.split( fields[ 5 ], "," ), | |
assigned_privs = string.split( fields[ 6 ], "," ), | |
sessions_count = tonumber( fields[ 7 ] ), | |
sessions_length = tonumber( fields[ 8 ] ), | |
attempts_success = tonumber( fields[ 9 ] ), | |
attempts_failure = tonumber( fields[ 10 ] ) | |
} | |
size = size + 1 | |
end | |
end | |
end | |
local db_commit = function ( ) | |
minetest.log( "action", "Writing authentication data to disk..." ) | |
--local t0 = os.clock( ) | |
--while os.clock( ) - t0 <= 10 do end | |
local file, errmsg = io.open( path .. "/~auth.db", "w+b" ) | |
if not file then | |
minetest.log( "error", "Cannot open " .. path .. "/~auth.db for writing." ) | |
error( "Fatal exception in AuthDatabase:commit( ), aborting." ) | |
end | |
for username, rec in pairs( data ) do | |
assert( file:write( table.concat( { | |
username, | |
rec.password, | |
rec.oldlogin, | |
rec.newlogin, | |
table.concat( rec.filtered_addrs, "," ), | |
table.concat( rec.assigned_privs, "," ), | |
rec.sessions_count, | |
rec.sessions_length, | |
rec.attempts_success, | |
rec.attempts_failure | |
}, ":" ) .. "\n" ) ) | |
end | |
file:close( ) | |
assert( os.rename( path .. "/~auth.db", path .. "/auth.db" ) ) | |
end | |
-- Public methods | |
self.connect = function ( ) | |
size = 0 | |
data = { } | |
users = { } | |
db_reload( ) | |
journal.audit( db_update, db_commit ) | |
journal.start( ) | |
end | |
self.disconnect = function ( ) | |
print( ":", journal.optime ) | |
for u, t in pairs( users ) do | |
data[ u ].sessions_length = data[ u ].sessions_length + ( journal.optime - t ) | |
end | |
-- check to see if this gets consistent session length with with long commit | |
db_commit( ) | |
print( ":", journal.optime ) | |
journal.reset( ) | |
data = nil | |
size = nil | |
users = nil | |
end | |
self.create = function ( username, password ) | |
local rec = | |
{ | |
password = password, | |
oldlogin = journal.optime, | |
newlogin = journal.optime, | |
filtered_addrs = { }, | |
assigned_privs = { }, | |
sessions_count = 0, | |
sessions_length = 0, | |
attempts_success = 0, | |
attempts_failure = 0 | |
} | |
data[ username ] = rec | |
size = size + 1 | |
journal.record_raw( TX_CREATE, username, password ) | |
end | |
self.delete = function ( username ) | |
data[ username ] = nil | |
size = size - 1 | |
journal.record_raw( TX_DELETE, username ) | |
end | |
self.set_password = function ( username, password ) | |
print( "set_password( )", username ) | |
data[ username ].password = password | |
journal.record_raw( TX_SET_PASSWORD, username, password ) | |
end | |
self.set_assigned_privs = function ( username, assigned_privs ) | |
print( "set_assigned_privs( )", username ) | |
data[ username ].assigned_privs = assigned_privs | |
journal.record_raw( TX_SET_ASSIGNED_PRIVS, username, table.concat( assigned_privs, "," ) ) | |
end | |
self.set_filtered_addrs = function ( username, filtered_addrs ) | |
print( "set_filtered_addrs( )", username ) | |
data[ username ].filtered_addrs = filtered_addrs | |
journal.record_raw( TX_SET_FILTERED_ADDRS, username, table.concat( filtered_addrs, "," ) ) | |
end | |
self.on_session_opened = function ( username ) | |
print( "on_session_opened( )", username ) | |
users[ username ] = journal.optime | |
data[ username ].sessions_count = data[ username ].sessions_count + 1 | |
journal.record_raw( TX_SESSION_OPENED, username ) | |
end | |
self.on_session_closed = function ( username ) | |
print( "on_session_closed( )", username ) | |
print( "sessions length:", journal.optime - users[ username ] ) | |
data[ username ].sessions_length = data[ username ].sessions_length + ( journal.optime - users[ username ] ) | |
users[ username ] = nil | |
journal.record_raw( TX_SESSION_CLOSED, username ) | |
end | |
self.on_attempt_failure = function ( username, ip ) | |
data[ username ].attempts_failure = data[ username ].attempts_failure + 1 | |
journal.record_raw( TX_ATTEMPT_FAILURE, username, ip ) | |
end | |
self.on_attempt_success = function ( username ) | |
print( "on_attempt_success( )", username ) | |
rec = data[ username ] | |
rec.newlogin = journal.optime | |
rec.attempts_success = rec.attempts_success + 1 | |
journal.record_raw( TX_ATTEMPT_SUCCESS, username ) | |
end | |
self.records = function ( ) | |
local k | |
return function ( ) | |
rec = next( data, k ) | |
k = rec.username | |
return rec.username, rec | |
end | |
end | |
self.records_match = function ( phrase ) | |
local k | |
return function ( ) | |
for k, rec in next( data, k ) do | |
if find_phrase( rec.username, phrase ) then | |
return rec | |
end | |
end | |
end | |
end | |
self.get_record_by_name = function ( username ) | |
print( "get_record_by_name( )", username ) | |
return data[ username ] | |
end | |
self.get_matches = function ( username ) | |
local uname = string.lower( username ) | |
local matches = { } | |
for cname in pairs( auth ) do | |
if string.lower( cname ) == uname then | |
table.insert( matches, cname ) | |
end | |
end | |
return matches | |
end | |
return self | |
end | |
----------------------------------------------------- | |
auth_db = AuthDatabase( minetest.get_worldpath( ) ) | |
auth_db.connect( ) | |
minetest.register_on_joinplayer( function ( player ) | |
auth_db.on_session_opened( player:get_player_name( ) ) | |
end ) | |
minetest.register_on_prejoinplayer( function ( player_name, ip ) | |
-- if not auth_db.get_record_by_name( username ) then | |
-- -- prevent case-insensitive duplicate accounts | |
-- local matches = auth_db.get_matches( username, true ) | |
-- if #matches > 0 then | |
-- return string.format( "A player named %s already exists on this server.", matches[ 1 ] ) | |
-- end | |
-- end | |
-- auth_db.on_attempt_initiate( player:get_player_name( ) ) | |
end ) | |
minetest.register_on_leaveplayer( function ( player ) | |
auth_db.on_session_closed( player:get_player_name( ) ) | |
end ) | |
minetest.register_on_authplayer( function ( player_name, ip, is_success ) | |
print( "on_auth_player( )", player_name, ip, is_success ) | |
-- auth_db.on_attempt_failure( player_name ) | |
end ) | |
minetest.register_on_shutdown( function( ) | |
auth_db.disconnect( ) | |
end ) | |
function unpack_privileges( assigned_privs ) | |
local privileges = { } | |
for _, p in pairs( assigned_privs ) do | |
privileges[ p ] = true | |
end | |
return privileges | |
end | |
function pack_privileges( privileges ) | |
local assigned_privs = { } | |
for p, b in pairs( privileges ) do | |
if b == true then | |
table.insert( assigned_privs, p ) | |
end | |
end | |
return assigned_privs | |
end | |
minetest.register_authentication_handler( { | |
-- translate old auth hooks to new database backend | |
get_auth = function( username ) | |
local rec = auth_db.get_record_by_name( username ) | |
return rec and { | |
password = rec.password, | |
privileges = unpack_privileges( rec.assigned_privs ), | |
last_login = rec.newlogin | |
} or nil | |
end, | |
create_auth = auth_db.create, | |
delete_auth = auth_db.delete, | |
set_password = auth_db.set_password, | |
set_privileges = function ( username, privileges ) | |
-- add callbacks | |
auth_db.set_assigned_privs( username, pack_privileges( privileges ) ) | |
-- minetest.notify_authentication_modified( username ) | |
end, | |
record_login = auth_db.on_attempt_success, | |
reload = function ( ) end, | |
iterate = auth_db.records | |
} ) | |
----------------------------------------------------- | |
-- File format of auth.db | |
-- ------------------------ | |
-- 1) username | |
-- 2) password | |
-- 3) oldlogin | |
-- 4) newlogin | |
-- 5) sessions_count | |
-- 6) sessions_length | |
-- 7) attempts_failure | |
-- 8) attempts_success | |
-- 9) filter_addrs | |
-- 10) assign_privs | |
-- | |
-- sorcerykid:1525391102:1525391102:#1#AACgQAAAoEAAAAAAAAAAAA#HO8LYSXVQpuSofh3gVaEWTBBqfWvpMs4x4JMx0OHZ4K9kMoQKIu804EAuo6jzvyXr00fZNV34aPo+OL3K0Sk9AznV2 | |
-- xmFC3UA9CxCoE8gAhmmo26FmXzAtDSGOCwgSRyjO8+2vPPHwXRObPeAyzF9rH+3bUiEJy8eoneuAzdTfqI+46o8AfRPsq+okxIGJa5Wd3GeBFQ5QbaGdUSRiVrKr8Lxg8L8J7l568mVBIQZ+gEEMc | |
-- Lv2mvvEInv/YjvNm5DFG4AdqwITVkswDWJApRW9KHQlaVTLchyk319hjGIXBDHTZHSkZJnkvpLTR1LW++nIMcYWkeAju/qYCU9UmN5w:192.168.142.*:shout,interact:423:59323:421:12 | |
-- | |
-- sorcerykid:#1#AACgQAAAoEAAAAAAAAAAAA#HO8LYSXVQpuSofh3gVaEWTBBqfWvpMs4x4JMx0OHZ4K9kMoQKIu804EAuo6jzvyXr00fZNV34aPo+OL3K0Sk9AznV2xmFC3UA9CxCoE8gAhmmo26 | |
-- FmXzAtDSGOCwgSRyjO8+2vPPHwXRObPeAyzF9rH+3bUiEJy8eoneuAzdTfqI+46o8AfRPsq+okxIGJa5Wd3GeBFQ5QbaGdUSRiVrKr8Lxg8L8J7l568mVBIQZ+gEEMcLv2mvvEInv/YjvNm5DFG4A | |
-- dqwITVkswDWJApRW9KHQlaVTLchyk319hjGIXBDHTZHSkZJnkvpLTR1LW++nIMcYWkeAju/qYCU9UmN5w:shout,interact:1525391102: | |
-- | |
-- File format of auth.dbx | |
-- ------------------------ | |
-- 1) optime | |
-- 2) opcode | |
-- 3) fields... | |
----------------------------------------------------- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment