Skip to content

Instantly share code, notes, and snippets.

@sorcerykid
Last active May 15, 2018 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sorcerykid/8e567ba1b4b737e79c2469d4cbe93d6f to your computer and use it in GitHub Desktop.
Save sorcerykid/8e567ba1b4b737e79c2469d4cbe93d6f to your computer and use it in GitHub Desktop.
--------------------------------------------------------
-- 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