diff --git a/README.txt b/README.txt index 2a4751f..e5715c8 100644 --- a/README.txt +++ b/README.txt @@ -1,4 +1,4 @@ -Auth Redux Mod v2.6b +Auth Redux Mod v2.7b By Leslie Krause Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest. @@ -71,6 +71,14 @@ Version 2.6b (19-Jul-2018) - tweaked lexer to skip comments on ruleset loading - added search function to AuthDatabase class +Version 2.7b (21-Jul-2018) + - implemented time and date datatypes for rulesets + - updated code samples with latest feature-set + - added time-related functions for use by rulesets + - added time-related variables for use by rulesets + - minor formatting fixes to source code + - optimized comparison algorithm in ruleset parser + Installation ---------------------- diff --git a/filter.lua b/filter.lua index 65898b7..b693f78 100644 --- a/filter.lua +++ b/filter.lua @@ -1,15 +1,19 @@ -------------------------------------------------------- --- Minetest :: Auth Redux Mod v2.6 (auth_rx) +-- Minetest :: Auth Redux Mod v2.7 (auth_rx) -- -- See README.txt for licensing and release notes. -- Copyright (c) 2017-2018, Leslie E. Krause -------------------------------------------------------- FILTER_TYPE_STRING = 11 -FILTER_TYPE_BOOLEAN = 12 -FILTER_TYPE_NUMBER = 13 +FILTER_TYPE_NUMBER = 12 +FILTER_TYPE_BOOLEAN = 13 FILTER_TYPE_PATTERN = 14 FILTER_TYPE_SERIES = 15 +FILTER_TYPE_PERIOD = 16 +FILTER_TYPE_MOMENT = 17 +FILTER_TYPE_DATESPEC = 18 +FILTER_TYPE_TIMESPEC = 19 FILTER_MODE_FAIL = 20 FILTER_MODE_PASS = 21 FILTER_BOOL_AND = 30 @@ -60,7 +64,12 @@ function AuthFilter( path, name ) ["size"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_SERIES }, def = function ( a ) return #a end }, ["elem"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_SERIES, FILTER_TYPE_NUMBER }, def = function ( a, b ) return a[ b ] or "" end }, ["split"] = { type = FILTER_TYPE_SERIES, args = { FILTER_TYPE_STRING, FILTER_TYPE_STRING }, def = function ( a, b ) return string.split( a, b, true ) end }, - } + ["time"] = { type = FILTER_TYPE_TIMESPEC, args = { FILTER_TYPE_MOMENT }, def = function ( a ) return a % 86400 end }, + ["date"] = { type = FILTER_TYPE_DATESPEC, args = { FILTER_TYPE_MOMENT }, def = function ( a ) return math.floor( a / 86400 ) end }, + ["age"] = { type = FILTER_TYPE_PERIOD, args = { FILTER_TYPE_MOMENT }, def = function ( a ) return os.time( ) - a end }, -- FIXME: use global clock variable + ["before"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_MOMENT, FILTER_TYPE_PERIOD }, def = function ( a, b ) return a - b end }, + ["after"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_MOMENT, FILTER_TYPE_PERIOD }, def = function ( a, b ) return a + b end }, + } ---------------------------- -- private methods @@ -169,6 +178,27 @@ function AuthFilter( path, name ) t = FILTER_TYPE_PATTERN v = decode_base64( ref[ 1 ] ) v = "^" .. string.gsub( v, ".", sanitizer ) .. "$" + elseif find_token( "^(%d+)([ydhms])$" ) then + local factor = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 } + t = FILTER_TYPE_PERIOD + v = tonumber( ref[ 1 ] ) * factor[ ref[ 2 ] ] + elseif find_token( "^([-+]%d+)([ydhms])$" ) then + local factor = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 } + local origin = string.byte( ref[ 1 ] ) == 45 and vars.clock.value or 0 + t = FILTER_TYPE_MOMENT + v = origin + tonumber( ref[ 1 ] ) * factor[ ref[ 2 ] ] + elseif find_token( "^(%d?%d):(%d%d):(%d%d)$" ) or find_token( "^(%d?%d):(%d%d)$" ) then + local timespec = { + isdst = true, day = 1, month = 1, year = 1970, hour = tonumber( ref[ 1 ] ), min = tonumber( ref[ 2 ] ), sec = ref[ 3 ] and tonumber( ref[ 3 ] ) or 0, + } + t = FILTER_TYPE_TIMESPEC + v = os.time( timespec ) + elseif find_token( "^(%d%d)%-(%d%d)%-(%d%d%d%d)$" ) then + local datespec = { + isdst = true, day = tonumber( ref[ 1 ] ), month = tonumber( ref[ 2 ] ), year = tonumber( ref[ 3 ] ), hour = 0, + } + t = FILTER_TYPE_DATESPEC + v = math.floor( os.time( datespec ) / 86400 ) elseif find_token( "^'([a-zA-Z0-9+/]*);$" ) then t = FILTER_TYPE_STRING v = decode_base64( ref[ 1 ] ) @@ -244,7 +274,8 @@ function AuthFilter( path, name ) 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( ) } + vars[ "clock" ] = { type = FILTER_TYPE_MOMENT, value = os.time( ) } + vars[ "epoch" ] = { type = FILTER_TYPE_MOMENT, value = 0 } for num, line in ipairs( src ) do local stmt = string.split( line, " ", false ) @@ -356,21 +387,24 @@ function AuthFilter( path, name ) return trace( "Unrecognized operands in ruleset", num ) end + -- only allow comparisons of appropriate and equivalent datatypes + local do_math = { [FILTER_TYPE_STRING] = false, [FILTER_TYPE_NUMBER] = true, [FILTER_TYPE_PERIOD] = true, [FILTER_TYPE_MOMENT] = true, [FILTER_TYPE_DATESPEC] = true, [FILTER_TYPE_TIMESPEC] = true } + 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_GT and oper1.type == oper2.type and do_math[ oper2.type ] then + expr = ( oper1.value > oper2.value ) + elseif comp == FILTER_COMP_GTE and oper1.type == oper2.type and do_math[ oper2.type ] then + expr = ( oper1.value >= oper2.value ) + elseif comp == FILTER_COMP_LT and oper1.type == oper2.type and do_math[ oper2.type ] then + expr = ( oper1.value < oper2.value ) + elseif comp == FILTER_COMP_LTE and oper1.type == oper2.type and do_math[ oper2.type ] 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( 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 ) - elseif comp == FILTER_COMP_GTE and oper1.type == FILTER_TYPE_NUMBER and oper2.type == FILTER_TYPE_NUMBER then - expr = ( oper1.value >= oper2.value ) - elseif comp == FILTER_COMP_LTE and oper1.type == FILTER_TYPE_NUMBER and oper2.type == FILTER_TYPE_NUMBER then - expr = ( oper1.value <= oper2.value ) else return trace( "Mismatched operands in ruleset", num ) end diff --git a/init.lua b/init.lua index 27e88b9..bc61099 100644 --- a/init.lua +++ b/init.lua @@ -1,5 +1,5 @@ -------------------------------------------------------- --- Minetest :: Auth Redux Mod v2.6 (auth_rx) +-- Minetest :: Auth Redux Mod v2.7 (auth_rx) -- -- See README.txt for licensing and release notes. -- Copyright (c) 2017-2018, Leslie E. Krause @@ -18,10 +18,10 @@ 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 + 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 @@ -55,15 +55,15 @@ minetest.register_on_prejoinplayer( function ( 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 + 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 }, + 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 }, privs_list = { type = FILTER_TYPE_SERIES, value = rec and rec.assigned_privs or { } }, @@ -74,6 +74,9 @@ minetest.register_on_prejoinplayer( function ( player_name, player_ip ) 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 }, owner = { type = FILTER_TYPE_STRING, value = get_minetest_config( "name" ) }, + 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 }, } ) return filter_err @@ -143,26 +146,26 @@ minetest.register_authentication_handler( { } ) minetest.register_chatcommand( "filter", { - description = "Enable or disable ruleset-based login filtering, or reload a ruleset definition.", - privs = { server = true }, - func = function( name, param ) - if param == "" then - return true, "Login filtering is currently " .. ( auth_filter.is_active( ) and "enabled" or "disabled" ) .. "." - elseif param == "disable" then + description = "Enable or disable ruleset-based login filtering, or reload a ruleset definition.", + privs = { server = true }, + func = function( name, param ) + if param == "" then + return true, "Login filtering is currently " .. ( auth_filter.is_active( ) and "enabled" or "disabled" ) .. "." + elseif param == "disable" then auth_filter.disable( ) minetest.log( "action", "Login filtering disabled by " .. name .. "." ) return true, "Login filtering is disabled." - elseif param == "enable" then + elseif param == "enable" then auth_filter.enable( ) minetest.log( "action", "Login filtering enabled by " .. name .. "." ) return true, "Login filtering is enabled." - elseif param == "reload" then + elseif param == "reload" then auth_filter.refresh( ) return true, "Ruleset definition was loaded successfully." else return false, "Unknown parameter specified." end - end + end } ) auth_db.connect( ) diff --git a/samples.mt b/samples.mt index a82f06c..52ad504 100644 --- a/samples.mt +++ b/samples.mt @@ -1,16 +1,3 @@ -##################################################################### -# -# disallow new players whenever server is overloaded -# -##################################################################### - -try "There are too many players online right now." - -fail all -if $is_new eq $true -if $cur_users gt 20 -continue - ##################################################################### # # only allow administrator access (by username or IP address) @@ -23,6 +10,8 @@ if $addr eq "172.16.100.2" if $name eq "admin" continue +fail now + ##################################################################### # # block a range of IP addresses using wildcards @@ -41,7 +30,7 @@ pass now ##################################################################### # -# only allow access from whitelisted users +# only allow access from whitelisted players # ##################################################################### @@ -56,7 +45,7 @@ fall now ##################################################################### # -# never allow access from blacklisted users +# never allow access from blacklisted players # ##################################################################### @@ -69,10 +58,133 @@ pass now ##################################################################### # -# notify users that the server is unavailable right now +# notify players that the server is unavailable right now # ##################################################################### try "The server is temporarily offline for maintenance." fail now + +##################################################################### +# +# disallow players with all uppercase names +# +##################################################################### + +try "Sorry, we do not accept all uppercase player names." + +fail all +$name eq uc($name) +continue + +pass now + +##################################################################### +# +# disallow players with very short or very long names +# +##################################################################### + +try "Sorry, this player name is too long or too short." + +fail any +$name->len() gt 20 +$name->len() lt 3 +continue + +pass now + +##################################################################### +# +# disallow users that appear to be bots or guests +# +##################################################################### + +try "Sorry, we do not accept autogenerated player names." + +fail any +if $name is /;*;*##/ +if $name is /;*;*###/ +if $name is /Player#/ +if $name is /Player##/ +if $name is /Guest#/ +if $name is /Guest##/ +continue + +pass now + +##################################################################### +# +# disallow new players when the server is near capacity +# +##################################################################### + +try "There are too many players online right now." + +fail all +$is_new eq $true +$cur_users gte $max_users->mul(0.8) +continue + +pass now + +##################################################################### +# +# prevent players from joining with a reserved name +# +##################################################################### + +try "Sorry, this acccount has been permanently restricted." + +fail all +$is_new eq $true +when ("moderator","server","client","owner","player","system","operator","minetest") is $name +continue + +pass now + +##################################################################### +# +# disallow players that have been inactive for 90 days +# +##################################################################### + +try "Sorry, this acccount has been disabled for inactivity." + +fail all +if $is_new eq $false +if age($newlogin) gt 90d +continue + +pass now + +##################################################################### +# +# disallow new players during the weekends +# +##################################################################### + +try "Sorry, we are not accepting new players at this time." + +fail now +if $is_new eq $true +when ("Sat","Sun") eq $clock->day() +continue + +pass now + +##################################################################### +# +# prevent players from spam-logging the server +# +##################################################################### + +try "You are doing that too much. Please wait awhile." + +fail all +if $is_new eq $false +if age($newlogin) lt 15s +continue + +pass now