diff --git a/README.txt b/README.txt index cc33de6..9a22fc3 100644 --- a/README.txt +++ b/README.txt @@ -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 ---------------------- diff --git a/filter.lua b/filter.lua index 02c5398..733627d 100644 --- a/filter.lua +++ b/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 ] ] diff --git a/init.lua b/init.lua index bc61099..b00b88c 100644 --- a/init.lua +++ b/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 ) diff --git a/watchdog.lua b/watchdog.lua new file mode 100644 index 0000000..7d96bea --- /dev/null +++ b/watchdog.lua @@ -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