Build 14
- 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
This commit is contained in:
parent
3f38fcecd0
commit
c74b12183f
@ -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
|
||||
----------------------
|
||||
|
||||
|
42
filter.lua
42
filter.lua
@ -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 ] ]
|
||||
|
27
init.lua
27
init.lua
@ -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
47
watchdog.lua
Normal 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
|
Loading…
x
Reference in New Issue
Block a user