- implemented address datatype for rulesets
- added more helper functions for use by rulesets
- fixed missing syntax checks in ruleset parser
- developed and integrated AuthWatchdog class
- added meta-variables for stateful login filtering
master
Leslie Krause 2018-07-26 17:28:14 -04:00
parent 3f38fcecd0
commit c74b12183f
4 changed files with 107 additions and 18 deletions

View File

@ -1,4 +1,4 @@
Auth Redux Mod v2.8b
Auth Redux Mod v2.9b
By Leslie Krause
Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
@ -88,6 +88,13 @@ Version 2.8b (24-Jul-2018)
- updated comparison algorithm in ruleset parser
- passed preset variables array to filter functions
Version 2.9b (26-Jul-2018)
- implemented address datatype for rulesets
- added more helper functions for use by rulesets
- fixed missing syntax checks in ruleset parser
- developed and integrated AuthWatchdog class
- added meta-variables for stateful login filtering
Installation
----------------------

View File

@ -1,12 +1,13 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.8 (auth_rx)
-- Minetest :: Auth Redux Mod v2.9 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
--------------------------------------------------------
FILTER_TYPE_STRING = 11
FILTER_TYPE_NUMBER = 12
FILTER_TYPE_STRING = 10
FILTER_TYPE_NUMBER = 11
FILTER_TYPE_ADDRESS = 12
FILTER_TYPE_BOOLEAN = 13
FILTER_TYPE_PATTERN = 14
FILTER_TYPE_SERIES = 15
@ -45,6 +46,9 @@ local redate = function ( ts )
x.isdst = false
return os.time( x )
end
local unpack_address = function ( addr )
return { math.floor( addr / 16777216 ), math.floor( ( addr % 16777216 ) / 65536 ), math.floor( ( addr % 65536 ) / 256 ), addr % 256 }
end
----------------------------
-- StringPattern class
@ -132,7 +136,7 @@ function AuthFilter( path, name )
["trim"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return b > 0 and string.sub( a, 1, -b - 1 ) or string.sub( a, -b + 1 ) end },
["crop"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return b > 0 and string.sub( a, 1, b ) or string.sub( a, b, -1 ) end },
["size"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_SERIES }, def = function ( v, a ) return #a end },
["elem"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_SERIES, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a[ b ] or "" end },
["elem"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_SERIES, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a[ b > 0 and b or #a + b + 1 ] or "" end },
["split"] = { type = FILTER_TYPE_SERIES, args = { FILTER_TYPE_STRING, FILTER_TYPE_STRING }, def = function ( v, a, b ) return string.split( a, b, true ) end },
["time"] = { type = FILTER_TYPE_TIMESPEC, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return redate( a - v.epoch.value ) % 86400 end },
["date"] = { type = FILTER_TYPE_DATESPEC, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return math.floor( redate( a - v.epoch.value ) / 86400 ) end },
@ -141,6 +145,9 @@ function AuthFilter( path, name )
["after"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_MOMENT, FILTER_TYPE_PERIOD }, def = function ( v, a, b ) return a + b end },
["day"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return os.date( "%a", a ) end },
["at"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return localtime( a ) or 0 end },
["ip"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_ADDRESS }, def = function ( v, a ) return table.concat( unpack_address( a ), "." ) end },
["count"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_SERIES, FILTER_TYPE_STRING }, def = function ( v, a, b ) local t = 0; for i, v in ipairs( a ) do if v == b then t = t + 1; end; end; return t end },
["clip"] = { type = FILTER_TYPE_SERIES, args = { FILTER_TYPE_SERIES, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) local x = { }; local s = b < 0 and #a + b + 1 or 0; for i = 0, math.abs( b ) do table.insert( x, a[ s + i ] ); end; return x; end },
}
----------------------------
@ -210,7 +217,7 @@ function AuthFilter( path, name )
end
elseif find_token( "^%$([a-zA-Z0-9_]+)$" ) then
local name = ref[ 1 ]
if not vars[ name ] then
if not vars[ name ] or vars[ name ].value == nil then
return nil
end
t = vars[ name ].type
@ -225,7 +232,7 @@ function AuthFilter( path, name )
for line in file:lines( ) do
table.insert( v, line )
end
elseif find_token( "^/([a-zA-Z0-9+/]*),([tds]);$" ) then
elseif find_token( "^/([a-zA-Z0-9+/]*),([stda]);$" ) then
t = FILTER_TYPE_PATTERN
local phrase = minetest.decode_base64( ref[ 1 ] )
if ref[ 2 ] == "s" then
@ -262,6 +269,11 @@ function AuthFilter( path, name )
local datespec = os.date( "*t", value )
return { datespec.day, datespec.month, datespec.year }
end )
elseif ref[ 2 ] == "a" then
phrase = string.split( phrase, ".", false )
v = NumberPattern( phrase, { [FILTER_TYPE_ADDRESS] = true }, { "%d?%d?%d", "%d?%d?%d", "%d?%d?%d", "%d?%d?%d" }, function ( value )
return unpack_address( value )
end )
end
if not v then
return nil
@ -298,7 +310,10 @@ function AuthFilter( path, name )
end )
elseif find_token( "^-?%d+$" ) or find_token( "^-?%d*%.%d+$" ) then
t = FILTER_TYPE_NUMBER
v = tonumber( token )
v = tonumber( ref[ 1 ] )
elseif find_token( "^(%d+)%.(%d+)%.(%d+)%.(%d+)$" ) then
t = FILTER_TYPE_ADDRESS
v = tonumber( ref[ 1 ] ) * 16777216 + tonumber( ref[ 2 ] ) * 65536 + tonumber( ref[ 3 ] ) * 256 + tonumber( ref[ 4 ] )
else
return nil
end
@ -329,7 +344,7 @@ function AuthFilter( path, name )
----------------------------
self.refresh = function ( )
local file = io.open( path .. "/" .. name, "rb" )
local file = io.open( path .. "/" .. name, "r" )
if not file then
error( "The specified ruleset file does not exist." )
end
@ -342,7 +357,7 @@ function AuthFilter( path, name )
line = string.gsub( line, "'(.-)'", function ( str )
return "'" .. encode_base64( str ) .. ";"
end )
line = string.gsub( line, "/(.-)/([tds]?)", function ( a, b )
line = string.gsub( line, "/(.-)/([stda]?)", function ( a, b )
return "/" .. encode_base64( a ) .. "," .. ( b == "" and "s" or b ) .. ";"
end )
line = string.gsub( line, "%b()", function ( str )
@ -374,12 +389,9 @@ function AuthFilter( path, name )
-- skip no-op statements
elseif stmt[ 1 ] == "continue" then
if not rule then return trace( "Unexpected 'continue' statement in ruleset", num ) end
if #stmt ~= 1 then return trace( "Invalid 'continue' statement in ruleset", num ) end
if rule == nil then
return trace( "No ruleset declared", num )
end
if evaluate( rule ) then
return ( rule.mode == FILTER_MODE_FAIL and note or nil )
end
@ -398,7 +410,7 @@ function AuthFilter( path, name )
note = oper.value
elseif stmt[ 1 ] == "pass" or stmt[ 1 ] == "fail" then
if rule then return trace( "Missing continue statement in ruleset", num ) end
if rule then return trace( "Missing 'continue' statement in ruleset", num ) end
if #stmt ~= 2 then return trace( "Invalid 'pass' or 'fail' statement in ruleset", num ) end
rule = { }
@ -419,6 +431,7 @@ function AuthFilter( path, name )
rule.expr = { }
elseif stmt[ 1 ] == "when" or stmt[ 1 ] == "until" then
if not rule then return trace( "Unexpected 'when' or 'until' statement in ruleset", num ) end
if #stmt ~= 4 then return trace( "Invalid 'when' or 'until' statement in ruleset", num ) end
local cond = ( { ["when"] = FILTER_COND_TRUE, ["until"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]
@ -462,6 +475,7 @@ function AuthFilter( path, name )
table.insert( rule.expr, expr )
elseif stmt[ 1 ] == "if" or stmt[ 1 ] == "unless" then
if not rule then return trace( "Unexpected 'if' or 'unless' statement in ruleset", num ) end
if #stmt ~= 4 then return trace( "Invalid 'if' or 'unless' statement in ruleset", num ) end
local cond = ( { ["if"] = FILTER_COND_TRUE, ["unless"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]

View File

@ -1,5 +1,5 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.7 (auth_rx)
-- Minetest :: Auth Redux Mod v2.9 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
@ -7,6 +7,7 @@
dofile( minetest.get_modpath( "auth_rx" ) .. "/filter.lua" )
dofile( minetest.get_modpath( "auth_rx" ) .. "/db.lua" )
dofile( minetest.get_modpath( "auth_rx" ) .. "/watchdog.lua" )
-----------------------------------------------------
-- Registered Authentication Handler
@ -14,6 +15,7 @@ dofile( minetest.get_modpath( "auth_rx" ) .. "/db.lua" )
local auth_filter = AuthFilter( minetest.get_worldpath( ), "greenlist.mt" )
local auth_db = AuthDatabase( minetest.get_worldpath( ), "auth.db" )
local auth_watchdog = AuthWatchdog( )
local get_minetest_config = core.setting_get -- backwards compatibility
@ -41,14 +43,21 @@ function pack_privileges( privileges )
return assigned_privs
end
function convert_ipv4( str )
local ref = string.split( str, ".", false )
return tonumber( ref[ 1 ] ) * 16777216 + tonumber( ref[ 2 ] ) * 65536 + tonumber( ref[ 3 ] ) * 256 + tonumber( ref[ 4 ] )
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 )
auth_watchdog.on_failure( convert_ipv4( player_ip ) )
end )
end
minetest.register_on_prejoinplayer( function ( player_name, player_ip )
local rec = auth_db.select_record( player_name )
local res = auth_watchdog.get_metadata( convert_ipv4( player_ip ) )
if rec then
auth_db.on_login_attempt( player_name, player_ip )
@ -56,7 +65,7 @@ minetest.register_on_prejoinplayer( function ( player_name, player_ip )
-- 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
if string.lower( cname ) == uname then
return string.format( "A player named %s already exists on this server.", cname )
end
end
@ -64,7 +73,7 @@ minetest.register_on_prejoinplayer( function ( player_name, player_ip )
local filter_err = auth_filter.process( {
name = { type = FILTER_TYPE_STRING, value = player_name },
addr = { type = FILTER_TYPE_STRING, value = player_ip },
addr = { type = FILTER_TYPE_ADDRESS, value = convert_ipv4( player_ip ) },
is_new = { type = FILTER_TYPE_BOOLEAN, value = rec == nil },
privs_list = { type = FILTER_TYPE_SERIES, value = rec and rec.assigned_privs or { } },
users_list = { type = FILTER_TYPE_SERIES, value = auth_db.search( true ) },
@ -77,8 +86,16 @@ minetest.register_on_prejoinplayer( function ( player_name, player_ip )
uptime = { type = FILTER_TYPE_PERIOD, value = minetest.get_server_uptime( ) },
oldlogin = { type = FILTER_TYPE_MOMENT, value = rec and rec.oldlogin or 0 },
newlogin = { type = FILTER_TYPE_MOMENT, value = rec and rec.newlogin or 0 },
ip_names_list = { type = FILTER_TYPE_SERIES, value = res.previous_names or { } },
ip_prelogin = { type = FILTER_TYPE_MOMENT, value = res.prelogin or 0 },
ip_oldcheck = { type = FILTER_TYPE_MOMENT, value = res.oldcheck or 0 },
ip_newcheck = { type = FILTER_TYPE_MOMENT, value = res.newcheck or 0 },
ip_failures = { type = FILTER_TYPE_NUMBER, value = res.count_failures or 0 },
ip_attempts = { type = FILTER_TYPE_NUMBER, value = res.count_attempts or 0 }
} )
auth_watchdog.on_attempt( convert_ipv4( player_ip ), player_name )
return filter_err
end )
@ -86,6 +103,10 @@ 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 )
minetest.after( 0.0, function ( )
-- hack since player status not immediately available on some MT versions
auth_watchdog.on_success( convert_ipv4( minetest.get_player_information( player_name ).address ) )
end )
end )
minetest.register_on_leaveplayer( function ( player )

47
watchdog.lua Normal file
View File

@ -0,0 +1,47 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.9 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
--------------------------------------------------------
----------------------------
-- AuthWatchdog Class
----------------------------
function AuthWatchdog( )
local self = { }
local clients = { }
self.get_metadata = function ( ip )
return clients[ ip ] or { }
end
self.on_failure = function ( ip )
local meta = clients[ ip ]
meta.count_failures = meta.count_failures + 1
meta.newcheck = os.time( )
if not meta.oldcheck then
meta.oldcheck = os.time( )
end
return meta
end
self.on_success = function ( ip )
clients[ ip ] = nil
end
self.on_attempt = function ( ip, name )
if not clients[ ip ] then
clients[ ip ] = { count_attempts = 0, count_failures = 0, previous_names = { } }
end
local meta = clients[ ip ]
meta.count_attempts = meta.count_attempts + 1
meta.prelogin = os.time( )
table.insert( meta.previous_names, name )
return meta
end
return self
end