- moved Journal and AuthDatabase classes into library
- added rollback function to AuthDatabase class
- reworked journal audit to support rollback option
- better encapsulated database commit function
- allowed for STOPPED opcode during database update
- various changes to error and action messages
- moved command-line scripts to separate directory
- included script to rollback database via journal
- included script to extract debug log into journal
This commit is contained in:
Leslie Krause 2018-07-13 22:42:00 -04:00
parent add47bd537
commit 5b5ccd7b51
7 changed files with 744 additions and 389 deletions

View File

@ -1,4 +1,4 @@
Auth Redux Mod v2.3b
Auth Redux Mod v2.4b
By Leslie Krause
Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
@ -43,6 +43,17 @@ Version 2.3b (08-Jul-2018)
- changed database search method to use Lua regexes
- removed hard-coded file names from database methods
Version 2.4b (13-Jul-2018)
- moved Journal and AuthDatabase classes into library
- added rollback function to AuthDatabase class
- reworked journal audit to support rollback option
- better encapsulated database commit function
- allowed for STOPPED opcode during database update
- various changes to error and action messages
- moved command-line scripts to separate directory
- included script to rollback database via journal
- included script to extract debug log into journal
Installation
----------------------

402
db.lua Normal file
View File

@ -0,0 +1,402 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.4 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
--------------------------------------------------------
----------------------------
-- Transaction Op Codes
----------------------------
local LOG_STARTED = 10 -- <timestamp> 10
local LOG_CHECKED = 11 -- <timestamp> 11
local LOG_STOPPED = 12 -- <timestamp> 12
local TX_CREATE = 20 -- <timestamp> 20 <username> <password>
local TX_DELETE = 21 -- <timestamp> 21 <username>
local TX_SET_PASSWORD = 40 -- <timestamp> 40 <username> <password>
local TX_SET_APPROVED_ADDRS = 41 -- <timestamp> 41 <username> <approved_addrs>
local TX_SET_ASSIGNED_PRIVS = 42 -- <timestamp> 42 <username> <assigned_privs>
local TX_SESSION_OPENED = 50 -- <timestamp> 50 <username>
local TX_SESSION_CLOSED = 51 -- <timestamp> 51 <username>
local TX_LOGIN_ATTEMPT = 30 -- <timestamp> 30 <username> <ip>
local TX_LOGIN_FAILURE = 31 -- <timestamp> 31 <username> <ip>
local TX_LOGIN_SUCCESS = 32 -- <timestamp> 32 <username>
----------------------------
-- Journal Class
----------------------------
function Journal( path, name, is_rollback )
local file, err = io.open( path .. "/" .. name, "r+b" )
local self = { }
local cursor = 0
local rtime = 1.0
-- TODO: Verify integrity of database index
if not file then
minetest.log( "error", "Cannot open " .. path .. "/" .. name .. " for writing." )
error( "Fatal exception in Journal( ), aborting." )
end
self.audit = function ( update_proc, is_rollback )
-- Advance to the last set of noncommitted transactions (if any)
if not is_rollback then
minetest.log( "action", "Advancing database transaction log...." )
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 )
end
-- Update the database with all noncommitted transactions
local meta = { }
minetest.log( "action", "Replaying database transaction log...." )
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
minetest.log( "action", "Resetting database transaction log..." )
file:seek( "set", cursor )
file:write( optime .. " " .. LOG_STOPPED .. "\n" )
return optime
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, ... }, " " ) .. "\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
----------------------------
function AuthDatabase( path, name )
local data, users, index
local self = { }
local journal = Journal( path, name .. "x" )
-- Private methods
local db_update = function( meta, optime, opcode, ... )
local fields = { ... }
if opcode == TX_CREATE then
local rec =
{
password = fields[ 2 ],
oldlogin = -1,
newlogin = -1,
lifetime = 0,
total_sessions = 0,
total_attempts = 0,
total_failures = 0,
approved_addrs = { },
assigned_privs = { },
}
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_APPROVED_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_LOGIN_ATTEMPT then
data[ fields[ 1 ] ].total_attempts = data[ fields[ 1 ] ].total_attempts + 1
elseif opcode == TX_LOGIN_FAILURE then
data[ fields[ 1 ] ].total_failures = data[ fields[ 1 ] ].total_failures + 1
elseif opcode == TX_LOGIN_SUCCESS then
if data[ fields[ 1 ] ].oldlogin == -1 then
data[ fields[ 1 ] ].oldlogin = optime
end
meta.users[ fields[ 1 ] ] = data[ fields[ 1 ] ].newlogin
data[ fields[ 1 ] ].newlogin = optime
elseif opcode == TX_SESSION_OPENED then
data[ fields[ 1 ] ].total_sessions = data[ fields[ 1 ] ].total_sessions + 1
elseif opcode == TX_SESSION_CLOSED then
data[ fields[ 1 ] ].lifetime = data[ fields[ 1 ] ].lifetime + ( optime - data[ fields[ 1 ] ].newlogin )
meta.users[ fields[ 1 ] ] = nil
elseif opcode == LOG_STARTED then
meta.users = { }
elseif opcode == LOG_CHECKED or opcode == LOG_STOPPED then
-- calculate leftover session lengths due to abnormal server termination
for u, t in pairs( meta.users ) do
data[ u ].lifetime = data[ u ].lifetime + ( optime - data[ u ].newlogin )
end
meta.users = nil
end
end
local db_reload = function ( )
minetest.log( "action", "Reading authentication data from disk..." )
local file, errmsg = io.open( path .. "/" .. name, "r+b" )
if not file then
minetest.log( "error", "Cannot open " .. path .. "/" .. name .. " for reading." )
error( "Fatal exception in AuthDatabase:db_reload( ), aborting." )
end
local head = assert( file:read( "*line" ) )
index = tonumber( string.match( head, "^auth_rx/2.1 @(%d+)$" ) )
if not index or index < 0 then
minetest.log( "error", "Invalid header in authentication database." )
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 ] ),
lifetime = tonumber( fields[ 5 ] ),
total_sessions = tonumber( fields[ 6 ] ),
total_attempts = tonumber( fields[ 7 ] ),
total_failures = tonumber( fields[ 8 ] ),
approved_addrs = string.split( fields[ 9 ], "," ),
assigned_privs = string.split( fields[ 10 ], "," ),
}
end
end
file:close( )
end
local db_commit = function ( )
minetest.log( "action", "Writing authentication data to disk..." )
local file, errmsg = io.open( path .. "/~" .. name, "w+b" )
if not file then
minetest.log( "error", "Cannot open " .. path .. "/~" .. name .. " for writing." )
error( "Fatal exception in AuthDatabase:db_commit( ), aborting." )
end
index = index + 1
file:write( "auth_rx/2.1 @" .. index .. "\n" )
for username, rec in pairs( data ) do
assert( file:write( table.concat( {
username,
rec.password,
rec.oldlogin,
rec.newlogin,
rec.lifetime,
rec.total_sessions,
rec.total_attempts,
rec.total_failures,
table.concat( rec.approved_addrs, "," ),
table.concat( rec.assigned_privs, "," ),
}, ":" ) .. "\n" ) )
end
file:close( )
assert( os.remove( path .. "/" .. name ) )
assert( os.rename( path .. "/~" .. name, path .. "/" .. name ) )
end
-- Public methods
self.rollback = function ( )
data = { }
db_reload( )
journal.audit( db_update, true )
db_commit( )
data = nil
end
self.connect = function ( )
data = { }
users = { }
db_reload( )
if journal.audit( db_update, false ) then
db_commit( )
end
journal.start( )
end
self.disconnect = function ( )
for u, t in pairs( users ) do
data[ u ].lifetime = data[ u ].lifetime + ( journal.optime - data[ u ].newlogin )
end
db_commit( )
journal.reset( )
data = nil
users = nil
end
self.create_record = function ( username, password )
-- don't allow clobbering existing users
if data[ username ] then return false end
local rec =
{
password = password,
oldlogin = -1,
newlogin = -1,
lifetime = 0,
total_sessions = 0,
total_attempts = 0,
total_failures = 0,
approved_addrs = { },
assigned_privs = { },
}
data[ username ] = rec
journal.record_raw( TX_CREATE, username, password )
return true
end
self.delete_record = function ( username )
-- don't allow deletion of online users or non-existent users
if not data[ username ] or users[ username ] then return false end
data[ username ] = nil
journal.record_raw( TX_DELETE, username )
return true
end
self.set_password = function ( username, password )
if not data[ username ] then return false end
data[ username ].password = password
journal.record_raw( TX_SET_PASSWORD, username, password )
return true
end
self.set_assigned_privs = function ( username, assigned_privs )
if not data[ username ] then return false end
data[ username ].assigned_privs = assigned_privs
journal.record_raw( TX_SET_ASSIGNED_PRIVS, username, table.concat( assigned_privs, "," ) )
return true
end
self.set_approved_addrs = function ( username, approved_addrs )
if not data[ username ] then return false end
data[ username ].approved_addrs = approved_addrs
journal.record_raw( TX_SET_APPROVED_ADDRS, username, table.concat( approved_addrs, "," ) )
return true
end
self.on_session_opened = function ( username )
data[ username ].total_sessions = data[ username ].total_sessions + 1
journal.record_raw( TX_SESSION_OPENED, username )
end
self.on_session_closed = function ( username )
data[ username ].lifetime = data[ username ].lifetime + ( journal.optime - data[ username ].newlogin )
users[ username ] = nil
journal.record_raw( TX_SESSION_CLOSED, username )
end
self.on_login_attempt = function ( username, ip )
data[ username ].total_attempts = data[ username ].total_attempts + 1
journal.record_raw( TX_LOGIN_ATTEMPT, username, ip )
end
self.on_login_failure = function ( username, ip )
data[ username ].total_failures = data[ username ].total_failures + 1
journal.record_raw( TX_LOGIN_FAILURE, username, ip )
end
self.on_login_success = function ( username, ip )
if data[ username ].oldlogin == -1 then
data[ username ].oldlogin = journal.optime
end
users[ username ] = data[ username ].newlogin
data[ username ].newlogin = journal.optime
journal.record_raw( TX_LOGIN_SUCCESS, username, ip )
end
self.records = function ( )
return pairs( data )
end
self.records_match = function ( pattern )
local k
return function ( )
local v
local p = string.lower( pattern )
while true do
k, v = next( data, k )
if not k then
return
elseif string.match( string.lower( k ), p ) then
return k, v
end
end
end
end
self.select_record = function ( username )
return data[ username ]
end
return self
end

391
init.lua
View File

@ -1,393 +1,12 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.3 (auth_rx)
-- Minetest :: Auth Redux Mod v2.4 (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
--------------------------------------------------------
dofile( minetest.get_modpath( "auth_rx" ) .. "/filter.lua" )
--dofile( minetest.get_modpath( "auth_rx" ) .. "/db.lua" )
----------------------------
-- Transaction Op Codes
----------------------------
local LOG_STARTED = 10 -- <timestamp> 10
local LOG_CHECKED = 11 -- <timestamp> 11
local LOG_STOPPED = 12 -- <timestamp> 12
local TX_CREATE = 20 -- <timestamp> 20 <username> <password>
local TX_DELETE = 21 -- <timestamp> 21 <username>
local TX_SET_PASSWORD = 40 -- <timestamp> 40 <username> <password>
local TX_SET_APPROVED_ADDRS = 41 -- <timestamp> 41 <username> <approved_addrs>
local TX_SET_ASSIGNED_PRIVS = 42 -- <timestamp> 42 <username> <assigned_privs>
local TX_SESSION_OPENED = 50 -- <timestamp> 50 <username>
local TX_SESSION_CLOSED = 51 -- <timestamp> 51 <username>
local TX_LOGIN_ATTEMPT = 30 -- <timestamp> 30 <username> <ip>
local TX_LOGIN_FAILURE = 31 -- <timestamp> 31 <username> <ip>
local TX_LOGIN_SUCCESS = 32 -- <timestamp> 32 <username>
----------------------------
-- Journal Class
----------------------------
local Journal = function ( path, name )
local file, err = io.open( path .. "/" .. name, "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, index )
-- Update the database with all noncommitted transactions
-- TODO: Verify integrity of database index
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, ... }, " " ) .. "\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, name )
local data, users, index
local self = { }
local journal = Journal( path, name .. "x" )
-- Private methods
local db_update = function( meta, optime, opcode, ... )
local fields = { ... }
if opcode == TX_CREATE then
local rec =
{
password = fields[ 2 ],
oldlogin = -1,
newlogin = -1,
lifetime = 0,
total_sessions = 0,
total_attempts = 0,
total_failures = 0,
approved_addrs = { },
assigned_privs = { },
}
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_APPROVED_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_LOGIN_ATTEMPT then
data[ fields[ 1 ] ].total_attempts = data[ fields[ 1 ] ].total_attempts + 1
elseif opcode == TX_LOGIN_FAILURE then
data[ fields[ 1 ] ].total_failures = data[ fields[ 1 ] ].total_failures + 1
elseif opcode == TX_LOGIN_SUCCESS then
if data[ fields[ 1 ] ].oldlogin == -1 then
data[ fields[ 1 ] ].oldlogin = optime
end
meta.users[ fields[ 1 ] ] = data[ fields[ 1 ] ].newlogin
data[ fields[ 1 ] ].newlogin = optime
elseif opcode == TX_SESSION_OPENED then
data[ fields[ 1 ] ].total_sessions = data[ fields[ 1 ] ].total_sessions + 1
elseif opcode == TX_SESSION_CLOSED then
data[ fields[ 1 ] ].lifetime = data[ fields[ 1 ] ].lifetime + ( optime - data[ fields[ 1 ] ].newlogin )
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 ].lifetime = data[ u ].lifetime + ( optime - data[ u ].newlogin )
end
meta.users = nil
end
end
local db_reload = function ( )
minetest.log( "action", "Reading authentication data from disk..." )
local file, errmsg = io.open( path .. "/" .. name, "r+b" )
if not file then
minetest.log( "error", "Cannot open " .. path .. "/" .. name .. " for reading." )
error( "Fatal exception in AuthDatabase:reload( ), aborting." )
end
local head = assert( file:read( "*line" ) )
index = tonumber( string.match( head, "^auth_rx/2.1 @(%d+)$" ) )
if not index or index < 0 then
minetest.log( "error", "Invalid header in authentication database." )
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 ] ),
lifetime = tonumber( fields[ 5 ] ),
total_sessions = tonumber( fields[ 6 ] ),
total_attempts = tonumber( fields[ 7 ] ),
total_failures = tonumber( fields[ 8 ] ),
approved_addrs = string.split( fields[ 9 ], "," ),
assigned_privs = string.split( fields[ 10 ], "," ),
}
end
end
file:close( )
end
local db_commit = function ( )
minetest.log( "action", "Writing authentication data to disk..." )
local file, errmsg = io.open( path .. "/~" .. name, "w+b" )
if not file then
minetest.log( "error", "Cannot open " .. path .. "/~" .. name .. " for writing." )
error( "Fatal exception in AuthDatabase:commit( ), aborting." )
end
index = index + 1
file:write( "auth_rx/2.1 @" .. index .. "\n" )
for username, rec in pairs( data ) do
assert( file:write( table.concat( {
username,
rec.password,
rec.oldlogin,
rec.newlogin,
rec.lifetime,
rec.total_sessions,
rec.total_attempts,
rec.total_failures,
table.concat( rec.approved_addrs, "," ),
table.concat( rec.assigned_privs, "," ),
}, ":" ) .. "\n" ) )
end
file:close( )
assert( os.remove( path .. "/" .. name ) )
assert( os.rename( path .. "/~" .. name, path .. "/" .. name ) )
end
-- Public methods
self.connect = function ( )
data = { }
users = { }
db_reload( )
journal.audit( db_update, db_commit, index )
journal.start( )
end
self.disconnect = function ( )
for u, t in pairs( users ) do
data[ u ].lifetime = data[ u ].lifetime + ( journal.optime - data[ u ].newlogin )
end
db_commit( )
journal.reset( )
data = nil
users = nil
end
self.create_record = function ( username, password )
-- don't allow clobbering existing users
if data[ username ] then return false end
local rec =
{
password = password,
oldlogin = -1,
newlogin = -1,
lifetime = 0,
total_sessions = 0,
total_attempts = 0,
total_failures = 0,
approved_addrs = { },
assigned_privs = { },
}
data[ username ] = rec
journal.record_raw( TX_CREATE, username, password )
return true
end
self.delete_record = function ( username )
-- don't allow deletion of online users or non-existent users
if not data[ username ] or users[ username ] then return false end
data[ username ] = nil
journal.record_raw( TX_DELETE, username )
return true
end
self.set_password = function ( username, password )
if not data[ username ] then return false end
data[ username ].password = password
journal.record_raw( TX_SET_PASSWORD, username, password )
return true
end
self.set_assigned_privs = function ( username, assigned_privs )
if not data[ username ] then return false end
data[ username ].assigned_privs = assigned_privs
journal.record_raw( TX_SET_ASSIGNED_PRIVS, username, table.concat( assigned_privs, "," ) )
return true
end
self.set_approved_addrs = function ( username, approved_addrs )
if not data[ username ] then return false end
data[ username ].approved_addrs = approved_addrs
journal.record_raw( TX_SET_APPROVED_ADDRS, username, table.concat( approved_addrs, "," ) )
return true
end
self.on_session_opened = function ( username )
data[ username ].total_sessions = data[ username ].total_sessions + 1
journal.record_raw( TX_SESSION_OPENED, username )
end
self.on_session_closed = function ( username )
data[ username ].lifetime = data[ username ].lifetime + ( journal.optime - data[ username ].newlogin )
users[ username ] = nil
journal.record_raw( TX_SESSION_CLOSED, username )
end
self.on_login_attempt = function ( username, ip )
data[ username ].total_attempts = data[ username ].total_attempts + 1
journal.record_raw( TX_LOGIN_ATTEMPT, username, ip )
end
self.on_login_failure = function ( username, ip )
data[ username ].total_failures = data[ username ].total_failures + 1
journal.record_raw( TX_LOGIN_FAILURE, username, ip )
end
self.on_login_success = function ( username, ip )
if data[ username ].oldlogin == -1 then
data[ username ].oldlogin = journal.optime
end
users[ username ] = data[ username ].newlogin
data[ username ].newlogin = journal.optime
journal.record_raw( TX_LOGIN_SUCCESS, username, ip )
end
self.records = function ( )
return pairs( data )
end
self.records_match = function ( pattern )
local k
return function ( )
local v
local p = string.lower( pattern )
while true do
k, v = next( data, k )
if not k then
return
elseif string.match( string.lower( k ), p ) then
return k, v
end
end
end
end
self.select_record = function ( username )
return data[ username ]
end
return self
end
dofile( minetest.get_modpath( "auth_rx" ) .. "/db.lua" )
-----------------------------------------------------
-- Registered Authentication Handler
@ -489,11 +108,7 @@ minetest.register_authentication_handler( {
end
end
return {
password = rec.password,
privileges = unpack_privileges( assigned_privs ),
last_login = rec.newlogin
}
return { password = rec.password, privileges = unpack_privileges( assigned_privs ), last_login = rec.newlogin }
end
end,
create_auth = function( username, password )

223
tools/extract.awk Normal file
View File

@ -0,0 +1,223 @@
#!/bin/awk -f
################################################################################
# Database Import Script for Auth Redux Mod (Step 2)
# ---------------------------------------------------
# WARNING: This script is to be run immediately after the database conversion.
#
# This script will extract player login activity from the specified 'debug.txt'
# file and produce an import journal in the world directory. For best results,
# the 'auth.txt' and 'debug.txt' files should be complete and current. If they
# are not entirely synchronous, then errors are likely to result.
#
# Lastly, run the rollback.lua script from the command line. This will replay
# the import journal against the newly converted database, applying all player
# login activity as it was derived from the corresponding debug log.
#
# Below is the required sequence of commands:
#
# > awk -f convert.awk -v mode=convert ~/.minetest/worlds/world/auth.txt
# > awk -f extract.awk ~/.minetest/worlds/world/auth.txt ~/.minetest/debug.txt
# > lua rollback.lua ~/.minetest/worlds/world/auth.db
#
# It is not necessary to operate on the original files. They can be moved into
# into a temporary subdirectory for use with all of these scripts.
#
# WARNING: The 'auth.dbx' file will be overwritten. This should not cause any
# problems if you began with a freshly converted database. When in doubt, then
# you may want to backup the 'auth.db' and 'auth.dbx' files as a precaution.
#
# For more detailed output, you can change the debug level as follows:
#
# > awk -f extract.awk -v debug=verbose ...
#
# This will display both errors and warnings. It might be helpful to redirect
# output to a temporary file for this purpose. To see only errors, the default
# debug level of 'terse' should be sufficient.
#
# Warnings occur if there are orphaned accounts in the 'auth.txt' file that do
# not appear in the corresponding debug log. This happens when the debug log
# is incomplete. Orphaned accounts will have no player login activity applied.
#
# Errors occur if the 'auth.txt' file is inconsistent with the debug log. This
# could be the result of a server crash or intentional deletion of accounts.
# Such accounts are deemed invalid and player login activity will be ignored.
# However, you should verify that you are using the correct 'auth.txt' file.
################################################################################
# USAGE EXAMPLE:
# awk -f extract.awk -v debug=terse /home/minetest/.minetest/worlds/world/auth.txt /home/minetest/.minetest/debug.txt
function get_timestamp( date_str, time_str ) {
return mktime( sprintf( "%d %d %d %d %d %d", \
substr( date_str, 1, 4 ), substr( date_str, 6, 2 ), substr( date_str, 9, 2 ), substr( time_str, 1, 2 ), substr( time_str, 4, 2 ), substr( time_str, 7, 2 ) ) );
}
function print_info( source, result ) {
if( is_verbose == 1 ) {
print "[" source "]", result;
}
}
function check_user( name ) {
if( name in db_users ) {
++db_users[ name ];
return 0;
}
if( !log_users[ name ] ) {
print "ERROR: Player '" name "' does not exist in auth.txt file.";
}
return ++log_users[ name ];
}
function trim( str ) {
return substr( str, 2, length( str ) - 2 )
}
BEGIN {
FS = " "
LOG_STARTED = 10
LOG_STOPPED = 12
LOG_TOUCHED = 13
TX_SESSION_OPENED = 50
TX_SESSION_CLOSED = 51
TX_LOGIN_FAILURE = 31
TX_LOGIN_SUCCESS = 32
journal_file = "auth.dbx";
is_started = 0;
is_verbose = 0;
total_records = 0;
if( debug == "verbose" ) {
is_verbose = 1;
}
else if( debug != "terse" ) {
print "ERROR: Unknown argument, defaulting to terse debug level.";
}
if( ARGC != 3 ) {
print( "The required arguments are missing, aborting." )
exit 1;
}
world_path = ARGV[ 1 ];
if( sub( /auth.txt$/, "", world_path ) == 0 ) {
print( "The specified auth.txt file is not recognized, aborting." )
exit 1;
}
if( ARGV[ 2 ] != "debug.txt" && ARGV[ 2 ] !~ /\/debug.txt$/ ) {
print( "The specified debug.txt file is not recognized, aborting." )
exit 1;
}
}
# parse the 'auth.txt' file
ARGIND == 1 && FNR == 1 {
print( "Reading the " world_path "auth.txt file..." )
}
ARGIND == 1 {
name = substr( $0, 1, index( $0, ":" ) - 1 )
db_users[ name ] = 0;
total_records++;
}
# parse the 'debug.txt' file
ARGIND == 2 && FNR == 1 {
print( "Reading the " world_path "debug.txt file..." )
}
ARGIND == 2 {
cur_date_str = $1;
cur_time_str = $2;
if( $3 == "ACTION[Main]:" && ( $4 FS $5 ) == "World at" ) {
# 2018-07-10 12:16:09: ACTION[Main]: World at [/root/.minetest/worlds/world]
print_info( "debug.txt", $1 " @connect" );
print get_timestamp( cur_date_str, cur_time_str ), LOG_STARTED > ( world_path journal_file );
is_started = 1;
}
else if( !is_started ) {
# sanity check since mod loading errors precede startup logging
# and shutdowns are not always immediate after they are logged.
next;
}
else if( $3 == "[Main]:" && $5 == "sigint_handler():" || $3 == "ERROR[Main]:" && ( $4 FS $5 ) == "stack traceback:" ) {
# 2018-07-10 06:47:46: [Main]: INFO: sigint_handler(): Ctrl-C pressed, shutting down.
# 2018-07-10 16:18:52: ERROR[Main]: stack traceback:
print_info( "debug.txt", $1 " @disconnect " ( $4 == "stack" ? "(err)" : "(sig)" ) );
print get_timestamp( cur_date_str, cur_time_str ), LOG_STOPPED > ( world_path journal_file );
is_started = 0;
}
else if( $3 == "ACTION[Server]:" && ( $5 == "joins" || $6 == "joins" || $5 == "leaves" || $5 == "times" || $5 == "shuts" || $4 == "Server:" ) ) { # optimization hack
cur_action_str = substr( $0, 38 );
if( cur_action_str ~ /^[a-zA-Z0-9_-]+ \[[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\] joins game\./ ) {
# 2017-06-09 16:49:26: ACTION[Server]: sorcerykid [127.0.0.1] joins game.
# print_info( "debug.txt", $1 " @on_login_success " $4 FS trim( $5 ) );
if( check_user( $4 ) > 0 ) next;
print get_timestamp( cur_date_str, cur_time_str ), TX_LOGIN_SUCCESS, $4, trim( $5 ) > ( world_path journal_file );
}
else if( cur_action_str ~ /^Server: User [a-zA-Z0-9_-]+ at [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ supplied wrong password/ ) {
# 2017-06-09 20:35:20: ACTION[Server]: Server: User sorcerykid at 127.0.0.1 supplied wrong password (auth mechanism: SRP)
# print_info( "debug.txt", $1 " @on_login_failure " $6 FS $8 );
if( check_user( $6 ) > 0 ) next;
print get_timestamp( cur_date_str, cur_time_str ), TX_LOGIN_FAILURE, $6, $8 > ( world_path journal_file );
}
else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ joins game\./ ) {
# 2017-06-09 16:49:26: ACTION[Server]: sorcerykid joins game. List of players:
# print_info( "debug.txt", $1 " @on_session_opened " $4 );
if( check_user( $4 ) > 0 ) next;
print get_timestamp( cur_date_str, cur_time_str ), TX_SESSION_OPENED, $4 > ( world_path journal_file );
}
else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ leaves game\./ || cur_action_str ~ /[a-zA-Z0-9_-]+ times out\./ ) {
# 2017-06-09 20:32:32: ACTION[Server]: sorcerykid leaves game. List of players:
# 2017-06-09 20:34:47: ACTION[Server]: sorcerykid times out. List of players:
# print_info( "debug.txt", $1 " @on_session_closed " $4 );
if( check_user( $4 ) > 0 ) next;
print get_timestamp( cur_date_str, cur_time_str ), TX_SESSION_CLOSED, $4 > ( world_path journal_file );
}
else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ shuts down server/ ) {
# 2017-06-09 20:32:32: ACTION[Server]: sorcerykid shuts down server
# print_info( "debug.txt", $1 " @disconnect (req)" );
print get_timestamp( cur_date_str, cur_time_str ), LOG_STOPPED > ( world_path journal_file );
is_started = 0;
}
}
}
END {
total_orphans = 0;
for( name in db_users ) {
if( db_users[ name ] == 0 ) {
if( is_verbose == 1 ) {
print "WARNING: No player activity for '" name "' in debug.txt file.";
}
total_orphans++;
}
}
print "Total accounts in database: " total_records " (" total_orphans " orphaned accounts)";
print "Done!"
}

38
tools/revert.awk Normal file
View File

@ -0,0 +1,38 @@
#!/bin/awk -f
################################################################################
# Database Export Script for Auth Redux Mod
# ------------------------------------------
# This script will revert to the default 'auth.txt' flat-file database required
# by the builtin authentication handler.
#
# EXAMPLE:
# awk -f revert.awk ~/.minetest/worlds/world/auth.db
################################################################################
BEGIN {
FS = ":";
OFS = ":";
db_file = "auth.txt";
path = ARGV[ 1 ]
if( sub( /[-_A-Za-z0-9]+\.db$/, "", path ) == 0 ) {
# sanity check for nonstandard input file
path = "";
}
print "Reverting " ARGV[ 1 ] "...";
}
NF == 10 {
username = $1;
password = $2;
assigned_privs = $10;
newlogin = $4;
print username, password, assigned_privs, newlogin > path db_file;
}
END {
print "Done!"
}

66
tools/rollback.lua Normal file
View File

@ -0,0 +1,66 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.4 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
--------------------------------------------------------
minetest = { }
function string.split( str, sep, has_nil )
res = { }
for val in string.gmatch( str .. sep, "(.-)" .. sep ) do
if val ~= "" or has_nil then
table.insert( res, val )
end
end
return res
end
minetest.log = function ( act, str )
print( "[" .. act .. "]", str )
end
minetest.register_globalstep = function ( ) end
--------------------------------------------------------
dofile( "../db.lua" )
local name = "auth.db"
local path = "."
print( "******************************************************" )
print( "* This script will rollback the Auth Redux database. *" )
print( "* Do not proceed unless you know what you are doing! *" )
print( "* -------------------------------------------------- *" )
print( "* Usage Example: *" )
print( "* lua rollback.lua ~/.minetest/worlds/world/auth.db *" )
print( "******************************************************" )
if arg[ 1 ] and arg[ 1 ] ~= "auth.db" then
path = string.match( arg[ 1 ], "^(.*)/auth%.db$" )
if not path then
error( "Invalid arguments specified." )
end
end
print( "The following database will be modified:" )
print( " " .. path .. "/" .. name )
print( )
io.write( "Do you wish to continue (y/n)? " )
local opt = io.read( 1 )
if opt == "y" then
print( "Initiating rollback procedure..." )
local auth_db = AuthDatabase( path, name )
auth_db.rollback( )
os.rename( path .. "/" .. name .. "x", path .. "/~" .. name .. "x" )
if not io.open( path .. "/" .. name .. "x", "w+b" ) then
minetest.log( "error", "Cannot open " .. path .. "/~" .. name .. " for writing." )
end
end