Build 01
- initial beta version
This commit is contained in:
commit
df42508ef6
49
README.txt
Normal file
49
README.txt
Normal 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
303
filter.lua
Normal 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
554
init.lua
Normal 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( )
|
Loading…
x
Reference in New Issue
Block a user