- initial beta version
This commit is contained in:
Leslie Krause 2018-06-30 19:14:52 -04:00
commit df42508ef6
3 changed files with 906 additions and 0 deletions

49
README.txt Normal file
View File

@ -0,0 +1,49 @@
Auth Redux Mod v2.1b
By Leslie E. Krause
Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
It is designed from the ground up to be robust and secure enough for use on high-traffic
Minetest servers, while also addressing a number of outstanding bugs (including #5334
and #6783 and #4451).
Auth Redux is intended to be compatible with all versions of Minetest 0.4.14+.
Revision History
----------------------
Version 2.1b (28-Jun-2018)
- initial beta version
Installation
----------------------
1) Unzip the archive into the mods directory of your game
2) Rename the auth_rx-master directory to "auth_rx"
3) Create an empty file named "auth.dbx" within the respective world directory
4) Create an empty file named "greenlistmt" within the respective world directory
Source Code License
----------------------
The MIT License (MIT)
Copyright (c) 2016-2018, Leslie E. Krause
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
For more details:
https://opensource.org/licenses/MIT

303
filter.lua Normal file
View File

@ -0,0 +1,303 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.1 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
--
-- ./games/minetest_game/mods/auth_rx/filter.lua
--------------------------------------------------------
FILTER_TYPE_STRING = 11
FILTER_TYPE_BOOLEAN = 12
FILTER_TYPE_NUMBER = 13
FILTER_TYPE_PATTERN = 14
FILTER_TYPE_SERIES = 15
FILTER_MODE_FAIL = 20
FILTER_MODE_PASS = 21
FILTER_BOOL_AND = 30
FILTER_BOOL_OR = 31
FILTER_BOOL_XOR = 32
FILTER_BOOL_NOW = 33
FILTER_COND_FALSE = 40
FILTER_COND_TRUE = 41
FILTER_COMP_EQ = 50
FILTER_COMP_GT = 51
FILTER_COMP_LT = 52
FILTER_COMP_IS = 53
----------------------------
-- AuthFilter class
----------------------------
function AuthFilter( path, name )
local src = { }
local opt = { is_debug = false, is_strict = true }
local self = { }
local file = io.open( path .. "/" .. name, "rb" )
if not file then
error( "The specified ruleset file does not exist." )
end
for line in file:lines( ) do
-- encode string and pattern literals to make parsing easier
line = string.gsub( line, "\"(.-)\"", function ( str )
return "\"" .. minetest.encode_base64( str )
end )
line = string.gsub( line, "'(.-)'", function ( str )
return "'" .. minetest.encode_base64( str )
end )
line = string.gsub( line, "/(.-)/", function ( str )
return "/" .. minetest.encode_base64( str )
end )
table.insert( src, line )
end
file:close( file )
----------------------------
-- private methods
----------------------------
local throw = function ( msg, num )
-- minetest.log( "error", msg .. " (line " .. num .. ")" )
error( msg .. " (line " .. num .. ")" )
end
local get_operand = function ( token, vars )
local t, v
if string.find( token, "^%$[a-zA-Z_]+$" ) then
local var = string.sub( token, 2 )
if not vars[ var ] then
return nil
else
t = vars[ var ].type
v = vars[ var ].value
end
elseif string.find( token, "^@[a-zA-Z._-]*$" ) then
t = FILTER_TYPE_SERIES
v = { }
local file = io.open( path .. "/" .. string.sub( token, 2 ), "rb" )
if not file then
return nil
end
for line in file:lines( ) do
table.insert( v, line )
end
elseif string.find( token, "^/.*$" ) then
-- sanitize search phrase and convert to regexp pattern
local sanitizer =
{
["["] = "";
["]"] = "";
["^"] = "%^";
["$"] = "%$";
["("] = "%(";
[")"] = "%)";
["%"] = "%%";
["."] = "%.";
["-"] = "%-";
["*"] = "[a-zA-Z0-9_-]*";
["+"] = "[a-zA-Z0-9_-]+";
["?"] = "[a-zA-Z0-9_-]";
["#"] = "%d";
["~"] = "%a";
}
t = FILTER_TYPE_PATTERN
v = minetest.decode_base64( string.sub( token, 2 ) )
v = "^" .. string.gsub( string.upper( v ), ".", sanitizer ) .. "$"
elseif string.find( token, "^'.*$" ) then
t = FILTER_TYPE_STRING
v = minetest.decode_base64( string.sub( token, 2 ) )
elseif string.find( token, "^\".*$" ) then
t = FILTER_TYPE_STRING
v = minetest.decode_base64( string.sub( token, 2 ) )
v = string.gsub( v, "%$([a-zA-Z_]+)", function ( var )
return vars[ var ] and tostring( vars[ var ].value ) or "?"
end )
elseif string.find( token, "^%d+$" ) then
t = FILTER_TYPE_NUMBER
v = tonumber( token )
else
return nil
end
return { type = t, value = v }
end
local evaluate = function ( rule )
-- short circuit binary logic to simplify evaluation
local res = ( rule.bool == FILTER_BOOL_AND )
local xor = 0
for i, v in ipairs( rule.expr ) do
if rule.bool == FILTER_BOOL_AND and not v then
return false
elseif rule.bool == FILTER_BOOL_OR and v then
return true
elseif rule.bool == FILTER_BOOL_XOR and v then
xor = xor + 1
end
end
if xor == 1 then return true end
return res
end
----------------------------
-- public methods
----------------------------
self.process = function( vars )
local rule
local note = "Access denied."
vars[ "true" ] = { type = FILTER_TYPE_BOOLEAN, value = true }
vars[ "false" ] = { type = FILTER_TYPE_BOOLEAN, value = false }
vars[ "time" ] = { type = FILTER_TYPE_NUMBER, value = os.time( ) }
for num, line in ipairs( src ) do
-- FIXME: ignore extraneous whitespace, even at beginning of line
local stmt = string.split( line, " ", false )
if string.byte( line ) == 35 or #stmt == 0 then
-- skip comments (lines beginning with hash character) and empty lines
-- TODO: these should be stripped on file import
elseif stmt[ 1 ] == "continue" then
if #stmt ~= 1 then throw( "Invalid 'continue' statement in ruleset", num ) end
if rule == nil then
throw( "No ruleset declared", num )
end
if evaluate( rule ) then
return ( rule.mode == FILTER_MODE_FAIL and note or nil )
end
rule = nil
elseif stmt[ 1 ] == "try" then
if rule then throw( "Missing 'continue' statement in ruleset", num ) end
if #stmt ~= 2 then throw( "Invalid 'try' statement in ruleset", num ) end
local oper = get_operand( stmt[ 2 ], vars )
if not oper then
throw( "Unrecognized operand in ruleset", num )
end
note = oper.value
elseif stmt[ 1 ] == "pass" or stmt[ 1 ] == "fail" then
if rule then throw( "Missing continue statement in ruleset", num ) end
if #stmt ~= 2 then throw( "Invalid 'pass' or 'fail' statement in ruleset", num ) end
rule = { }
local mode = ( { ["pass"] = FILTER_MODE_PASS, ["fail"] = FILTER_MODE_FAIL } )[ stmt[ 1 ] ]
local bool = ( { ["all"] = FILTER_BOOL_AND, ["any"] = FILTER_BOOL_OR, ["one"] = FILTER_BOOL_XOR, ["now"] = FILTER_BOOL_NOW } )[ stmt[ 2 ] ]
if not mode or not bool then
throw( "Unrecognized keywords in ruleset", num )
end
if bool == FILTER_BOOL_NOW then
return ( mode == FILTER_MODE_FAIL and note or nil )
end
rule.mode = mode
rule.bool = bool
rule.expr = { }
elseif stmt[ 1 ] == "when" or stmt[ 1 ] == "until" then
if #stmt ~= 4 then throw( "Invalid 'when' or 'until' statement in ruleset", num ) end
local cond = ( { ["when"] = FILTER_COND_TRUE, ["until"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]
local comp = ( { ["eq"] = FILTER_COMP_EQ, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
if not cond or not comp then
throw( "Unrecognized keywords in ruleset", num )
end
local oper1 = get_operand( stmt[ 2 ], vars )
local oper2 = get_operand( stmt[ 4 ], vars )
if not oper1 or not oper2 then
throw( "Unrecognized operands in ruleset", num )
elseif oper1.type ~= FILTER_TYPE_SERIES then
throw( "Mismatched operands in ruleset", num )
end
-- cache second operand value for efficiency
-- TODO: might want to move the redundant operand type checks out of loop?
local value2 = ( comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_STRING ) and string.upper( oper2.value ) or oper2.value
local expr = false
for i, value1 in ipairs( oper1.value ) do
if comp == FILTER_COMP_EQ and oper2.type == FILTER_TYPE_STRING then
expr = ( value1 == value2 )
elseif comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_STRING then
expr = ( string.upper( value1 ) == value2 )
elseif comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_PATTERN then
expr = ( string.find( string.upper( value1 ), value2 ) == 1 )
else
throw( "Mismatched operands in ruleset", num )
end
if expr then break end
end
if cond == FILTER_COND_FALSE then expr = not expr end
table.insert( rule.expr, expr )
elseif stmt[ 1 ] == "if" or stmt[ 1 ] == "unless" then
if #stmt ~= 4 then throw( "Invalid 'if' or 'unless' statement in ruleset", num ) end
local cond = ( { ["if"] = FILTER_COND_TRUE, ["unless"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]
local comp = ( { ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
if not cond or not comp then
throw( "Unrecognized keywords in ruleset", num )
end
local oper1 = get_operand( stmt[ 2 ], vars )
local oper2 = get_operand( stmt[ 4 ], vars )
if not oper1 or not oper2 then
throw( "Unrecognized operands in ruleset", num )
end
-- FIXME: don't allow equality comparison of patterns or series
local expr
if comp == FILTER_COMP_EQ and oper1.type == oper2.type and oper1.type ~= FILTER_TYPE_SERIES and oper1.type ~= FILTER_TYPE_PATTERN then
expr = ( oper1.value == oper2.value )
elseif comp == FILTER_COMP_IS and oper1.type == FILTER_TYPE_STRING and oper2.type == FILTER_TYPE_STRING then
expr = ( string.upper( oper1.value ) == string.upper( oper2.value ) )
elseif comp == FILTER_COMP_IS and oper1.type == FILTER_TYPE_STRING and oper2.type == FILTER_TYPE_PATTERN then
expr = ( string.find( string.upper( oper1.value ), oper2.value ) == 1 )
elseif comp == FILTER_COMP_GT and oper1.type == FILTER_TYPE_NUMBER and oper2.type == FILTER_TYPE_NUMBER then
expr = ( oper1.value > oper2.value )
elseif comp == FILTER_COMP_LT and oper1.type == FILTER_TYPE_NUMBER and oper2.type == FILTER_TYPE_NUMBER then
expr = ( oper1.value < oper2.value )
else
throw( "Mismatched operands in ruleset", num )
end
if cond == FILTER_COND_FALSE then expr = not expr end
table.insert( rule.expr, expr )
-- TODO: immediately evaluating each expression (thus avoiding a list) would be optimal,
-- but probably requires state table; efficiency vs complexity scenario
else
throw( "Invalid statement in ruleset", num )
end
end
throw( "Unexpected end-of-file in ruleset", num )
end
return self
end

554
init.lua Normal file
View File

@ -0,0 +1,554 @@
--------------------------------------------------------
-- 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
--------------------------------------------------------
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, size, users, index
local self = { }
local journal = Journal( path, name .. "x" )
-- 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( source, ( string.gsub( phrase, ".", sanitizer ) ) )
end
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 .. "/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
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 ], "," ),
}
size = size + 1
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 .. "/auth.db" ) )
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, 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
size = 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
size = size + 1
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
size = size - 1
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 ( phrase )
local k
return function ( )
local v
local p = string.lower( phrase )
k, v = next( data, k )
if find_phrase( string.lower( k ), p ) then
return k, v
end
end
end
self.select_record = function ( username )
return data[ username ]
end
return self
end
-----------------------------------------------------
-- Registered Authentication Handler
-----------------------------------------------------
local auth_filter = AuthFilter( minetest.get_worldpath( ), "greenlist.mt" )
local auth_db = AuthDatabase( minetest.get_worldpath( ), "auth.db" )
local get_minetest_config = core.setting_get -- backwards compatibility
function get_default_privs( )
local default_privs = { }
for _, p in pairs( string.split( get_minetest_config( "default_privs" ), "," ) ) do
table.insert( default_privs, string.trim( p ) )
end
return default_privs
end
function unpack_privileges( assigned_privs )
local privileges = { }
for _, p in ipairs( assigned_privs ) do
privileges[ p ] = true
end
return privileges
end
function pack_privileges( privileges )
local assigned_privs = { }
for p, _ in pairs( privileges ) do
table.insert( assigned_privs, p )
end
return assigned_privs
end
if minetest.register_on_auth_fail then
minetest.register_on_auth_fail( function ( player_name, player_ip )
auth_db.on_login_failure( player_name, player_ip )
end )
end
minetest.register_on_prejoinplayer( function ( player_name, player_ip )
local rec = auth_db.select_record( player_name )
if rec then
auth_db.on_login_attempt( player_name, player_ip )
else
-- prevent creation of case-insensitive duplicate accounts
local uname = string.lower( player_name )
for cname in auth_db.records( ) do
if string.lower( cname ) == uname then
return string.format( "A player named %s already exists on this server.", cname )
end
end
end
local filter_err = auth_filter.process( {
name = { type = FILTER_TYPE_STRING, value = player_name },
addr = { type = FILTER_TYPE_STRING, value = player_ip },
is_new = { type = FILTER_TYPE_BOOLEAN, value = rec == nil },
priv_list = { type = FILTER_TYPE_SERIES, value = rec and rec.assigned_privs or { } },
addr_list = { type = FILTER_TYPE_SERIES, value = rec and rec.approved_addrs or { } },
cur_users = { type = FILTER_TYPE_NUMBER, value = #minetest.get_connected_players( ) },
max_users = { type = FILTER_TYPE_NUMBER, value = get_minetest_config( "max_users" ) },
lifetime = { type = FILTER_TYPE_NUMBER, value = rec and rec.lifetime or 0 },
failures = { type = FILTER_TYPE_NUMBER, value = rec and rec.total_failures or 0 },
attempts = { type = FILTER_TYPE_NUMBER, value = rec and rec.total_attempts or 0 },
} )
-- TODO: Add optional filter logging capabilities
return filter_err
end )
minetest.register_on_joinplayer( function ( player )
local player_name = player:get_player_name( )
auth_db.on_login_success( player_name, "0.0.0.0" )
auth_db.on_session_opened( player_name )
end )
minetest.register_on_leaveplayer( function ( player )
auth_db.on_session_closed( player:get_player_name( ) )
end )
minetest.register_on_shutdown( function( )
auth_db.disconnect( )
end )
minetest.register_authentication_handler( {
-- translate old auth hooks to new database backend
get_auth = function( username )
local rec = auth_db.select_record( username )
if rec then
local assigned_privs = rec.assigned_privs
if get_minetest_config( "name" ) == username then
-- grant server operator all privileges
-- (TODO: implement as function that honors give_to_admin flag)
assigned_privs = { }
for priv in pairs( core.registered_privileges ) do
table.insert( assigned_privs, priv )
end
end
return {
password = rec.password,
privileges = unpack_privileges( assigned_privs ),
last_login = rec.newlogin
}
end
end,
create_auth = function( username, password )
if auth_db.create_record( username, password ) then
auth_db.set_assigned_privs( username, get_default_privs( ) )
minetest.log( "info", "Created player '" .. username .. "' in authentication database" )
end
end,
delete_auth = function( username )
if auth_db.delete_record( username ) then
minetest.log( "info", "Deleted player '" .. username .. "' in authenatication database" )
end
end,
set_password = function ( username, password )
if auth_db.set_password( username, password ) then
minetest.log( "info", "Reset password of player '" .. username .. "' in authentication database" )
end
end,
set_privileges = function ( username, privileges )
-- server operator's privileges are immutable
if get_minetest_config( "name" ) == username then return end
if auth_db.set_assigned_privs( username, pack_privileges( privileges ) ) then
minetest.notify_authentication_modified( username )
minetest.log( "info", "Reset privileges of player '" .. username .. "' in authentication database" )
end
end,
record_login = function ( ) end,
reload = function ( ) end,
iterate = auth_db.records
} )
auth_db.connect( )