- developed in-game database management console
- combined generic filter functions into superclass
- updated debugger to use new GenericFilter class
- added flag for constant-value operands in rulesets
- simplified operand matching logic in rulesets
- optimized comparison algorithm in ruleset parser
- consolidated lookup tables of lexical analyzer
- fixed erroneous status message shown in debugger
- added support for per-player debugging sessions
- redesigned login filter APIs for new architecture
- switched order of return values in login filter
- various code refactoring and better comments
master
Leslie Krause 2018-08-20 15:40:00 -04:00
parent 012fd491c3
commit cbd4f2d26c
4 changed files with 692 additions and 223 deletions

View File

@ -1,4 +1,4 @@
Auth Redux Mod v2.12
Auth Redux Mod v2.13
By Leslie Krause
Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
@ -105,12 +105,26 @@ Version 2.10b (29-Jul-2018)
- added missing preset variable needed by rulesets
Version 2.11 (04-Aug-2018)
- included a command-line player analytics script
- developed a command-line player analytics script
Version 2.12 (10-Aug-2018)
- better code consolidation of AuthFilter class
- reworked ruleset parser to support syntax changes
Version 2.13 (20-Aug-2018)
- developed in-game database management console
- combined generic filter functions into superclass
- updated debugger to use new GenericFilter class
- added flag for constant-value operands in rulesets
- simplified operand matching logic in rulesets
- optimized comparison algorithm in ruleset parser
- consolidated lookup tables of lexical analyzer
- fixed erroneous status message shown in debugger
- added support for per-player debugging sessions
- redesigned login filter APIs for new architecture
- switched order of return values in login filter
- various code refactoring and better comments
Installation
----------------------

View File

@ -1,14 +1,10 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.10 (auth_rx)
-- Minetest :: Auth Redux Mod v2.13 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
--------------------------------------------------------
-----------------------------------------------------
-- Registered Chat Commands
-----------------------------------------------------
local auth_db, auth_filter -- imported
minetest.register_chatcommand( "filter", {
@ -16,13 +12,13 @@ minetest.register_chatcommand( "filter", {
privs = { server = true },
func = function( name, param )
if param == "" then
return true, "Login filtering is currently " .. ( auth_filter.is_active( ) and "enabled" or "disabled" ) .. "."
return true, "Login filtering is currently " .. ( auth_filter.is_enabled and "enabled" or "disabled" ) .. "."
elseif param == "disable" then
auth_filter.disable( )
auth_filter.is_enabled = false
minetest.log( "action", "Login filtering disabled by " .. name .. "." )
return true, "Login filtering is disabled."
elseif param == "enable" then
auth_filter.enable( )
auth_filter.is_enabled = true
minetest.log( "action", "Login filtering enabled by " .. name .. "." )
return true, "Login filtering is enabled."
elseif param == "reload" then
@ -38,7 +34,9 @@ minetest.register_chatcommand( "fdebug", {
description = "Start an interactive debugger for testing ruleset definitions.",
privs = { server = true },
func = function( name, param )
if not minetest.create_form then return false, "This feature is not supported." end
if not minetest.create_form then
return false, "This feature is not supported."
end
local epoch = os.time( { year = 1970, month = 1, day = 1, hour = 0 } )
local vars = {
@ -71,9 +69,11 @@ minetest.register_chatcommand( "fdebug", {
local has_output = true
local login_index = 2
local var_index = 1
local temp_file = io.open( minetest.get_worldpath( ) .. "/~greenlist.mt", "w" ):close( )
local temp_filter = AuthFilter( minetest.get_worldpath( ), "~greenlist.mt", function ( err, num )
return num, "The server encountered an internal error.", err
local translate = GenericFilter( ).translate
local temp_name = "~greenlist_" .. minetest.encode_base64( name ) .. ".mt"
local temp_file = io.open( minetest.get_worldpath( ) .. "/" .. temp_name, "w" ):close( )
local temp_filter = AuthFilter( minetest.get_worldpath( ), temp_name, function ( err, num )
return "The server encountered an internal error.", num, err
end )
local function clear_prompts( buffer, has_single )
@ -207,7 +207,7 @@ minetest.register_chatcommand( "fdebug", {
local buffer = clear_prompts( fields.buffer .. "\n", true ) -- we need a trailing newline, or things will break
-- output ruleset to temp file for processing
local temp_file = io.open( minetest.get_worldpath( ) .. "/~greenlist.mt", "w" )
local temp_file = io.open( minetest.get_worldpath( ) .. "/" .. temp_name, "w" )
temp_file:write( buffer )
temp_file:close( )
temp_filter.refresh( )
@ -230,7 +230,7 @@ minetest.register_chatcommand( "fdebug", {
-- process ruleset and benchmark performance
local t = minetest.get_us_time( )
local num, res, err = temp_filter.process( vars )
local res, num, err = temp_filter.process( vars )
t = ( minetest.get_us_time( ) - t ) / 1000
if err then
@ -251,7 +251,7 @@ minetest.register_chatcommand( "fdebug", {
elseif fields.login_mode == "Wrong Password" then
if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset failed" ) end
status = { type = "ACTION", desc = string.format( "Ruleset failed at line %d (took %0.1f ms).", num, t ), user = has_output and "Invalid password" }
status = { type = "ACTION", desc = string.format( "Ruleset passed at line %d (took %0.1f ms).", num, t ), user = has_output and "Invalid password" }
vars.failures.value = vars.failures.value + 1
vars.ip_attempts.value = vars.ip_attempts.value + 1
@ -301,7 +301,7 @@ minetest.register_chatcommand( "fdebug", {
vars[ var_name ].is_auto = ( fields.var_is_auto == "true" )
elseif fields.set_var then
local oper = temp_filter.translate( string.trim( fields.var_value ), vars )
local oper = translate( string.trim( fields.var_value ), vars )
local var_name = vars_list[ var_index ]
if oper and var_name == "__debug" and datatypes[ oper.type ] then
@ -326,6 +326,415 @@ minetest.register_chatcommand( "fdebug", {
end,
} )
minetest.register_chatcommand( "auth", {
description = "Open the authentication database management console.",
privs = { server = true },
func = function( name, param )
local base_filter = GenericFilter( )
local epoch = os.time( { year = 1970, month = 1, day = 1, hour = 0 } )
local is_sort_reverse = false
local vars_list = { "username", "password", "oldlogin", "newlogin", "lifetime", "total_sessions", "total_failures", "total_attempts", "assigned_privs" }
local columns_list = { "$username", "$oldlogin->cal('D-MM-YY')", "$newlogin->cal('D-MM-YY')", "$lifetime->when('h')", "$total_sessions->str()", "$total_attempts->str()", "$total_failures->str()", "$assigned_privs->join(',')" }
local results_list
local selects_list
local var_index = 1
local var_input = ""
local select_index
local select_input = ""
local result_index
local results_horz
local results_vert
local column_index = 1
local column_macro = ""
base_filter.define_func( "str", FILTER_TYPE_STRING, { FILTER_TYPE_NUMBER },
function ( v, a ) return tostring( a ) end )
base_filter.define_func( "join", FILTER_TYPE_STRING, { FILTER_TYPE_SERIES, FILTER_TYPE_STRING },
function ( v, a, b ) return table.concat( a, b ) end )
base_filter.define_func( "when", FILTER_TYPE_STRING, { FILTER_TYPE_PERIOD, FILTER_TYPE_STRING },
function ( v, a, b ) local f = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 }; return f[ b ] and ( math.floor( a / f[ b ] ) .. b ) or "?" end )
base_filter.define_func( "cal", FILTER_TYPE_STRING, { FILTER_TYPE_MOMENT, FILTER_TYPE_STRING },
function ( v, a, b ) local f = { ["Y"] = "%y", ["YY"] = "%Y", ["M"] = "%m", ["MM"] = "%b", ["D"] = "%d", ["DD"] = "%a", ["h"] = "%H", ["m"] = "%M", ["s"] = "%S" }; return os.date( string.gsub( b, "%a+", f ), a ) end )
local function get_record_vars( username )
local rec = auth_db.select_record( username )
return rec and {
username = { value = username, type = FILTER_TYPE_STRING },
password = { value = rec.password, type = FILTER_TYPE_STRING },
oldlogin = { value = rec.oldlogin, type = FILTER_TYPE_MOMENT },
newlogin = { value = rec.newlogin, type = FILTER_TYPE_MOMENT },
lifetime = { value = rec.lifetime, type = FILTER_TYPE_PERIOD },
total_sessions = { value = rec.total_sessions, type = FILTER_TYPE_NUMBER },
total_failures = { value = rec.total_failures, type = FILTER_TYPE_NUMBER },
total_attempts = { value = rec.total_attempts, type = FILTER_TYPE_NUMBER },
assigned_privs = { value = rec.assigned_privs, type = FILTER_TYPE_SERIES },
} or { username = { value = username, type = FILTER_TYPE_STRING } }
end
local function reset_results( )
result_index = 1
results_vert = 0
results_horz = 0
results_list = auth_db.search( false )
select_index = 1
selects_list = { { input = "(default)", cache = results_list } }
end
local function query_results( input )
local stmt = string.split( base_filter.tokenize( input ), " ", false )
if #stmt ~= 4 then
return "Invalid 'if' or 'unless' statement in selector"
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, ["gte"] = FILTER_COMP_GTE, ["lte"] = FILTER_COMP_LTE, ["in"] = FILTER_COMP_IN, ["is"] = FILTER_COMP_IS, ["has"] = FILTER_COMP_HAS } )[ stmt[ 3 ] ]
if not cond or not comp then
return "Unrecognized keywords in selector"
end
-- initalize variables prior to loop (huge performance boost)
local vars = {
username = { type = FILTER_TYPE_STRING },
password = { type = FILTER_TYPE_STRING },
oldlogin = { type = FILTER_TYPE_MOMENT },
newlogin = { type = FILTER_TYPE_MOMENT },
lifetime = { type = FILTER_TYPE_PERIOD },
total_sessions = { type = FILTER_TYPE_NUMBER },
total_failures = { type = FILTER_TYPE_NUMBER },
total_attempts = { type = FILTER_TYPE_NUMBER },
assigned_privs = { type = FILTER_TYPE_SERIES },
}
base_filter.add_preset_vars( vars )
local refs1, refs2, proc1, proc2, oper1, oper2
local get_result = base_filter.get_result
local get_operand_parser = base_filter.get_operand_parser
local select_record = auth_db.select_record
local res = { }
for i, username in ipairs( results_list ) do
local rec = select_record( username )
if not rec then
return "Attempt to index a non-existent record"
end
vars.username.value = username
vars.password.value = rec.password
vars.oldlogin.value = rec.oldlogin
vars.newlogin.value = rec.newlogin
vars.lifetime.value = rec.lifetime
vars.total_sessions.value = rec.total_sessions
vars.total_failures.value = rec.total_failures
vars.total_attempts.value = rec.total_attempts
vars.assigned_privs.value = rec.assigned_privs
if not oper1 then
-- get parser on first iteration
if not proc1 then
proc1, refs1 = get_operand_parser( stmt[ 2 ] )
end
oper1 = proc1 and proc1( refs1, vars )
end
if not oper2 then
-- get parser on first iteration
if not proc2 then
proc2, refs2 = get_operand_parser( stmt[ 4 ] )
end
oper2 = proc2 and proc2( refs2, vars )
end
if not oper1 or not oper2 then
return "Unrecognized operands in selector"
end
local expr = get_result( cond, comp, oper1, oper2 )
if expr == nil then
return "Mismatched operands in selector"
end
-- add matching records to results
if expr then
table.insert( res, username )
end
-- cache operands that are constant
if not oper1.const then oper1 = nil end
if not oper2.const then oper2 = nil end
end
result_index = 1
results_list = res
results_vert = 0
select_index = select_index + 1
table.insert( selects_list, select_index, { input = input, cache = results_list } )
end
local function format_value( oper )
if oper.type == FILTER_TYPE_STRING then
return "\"" .. oper.value .. "\""
elseif oper.type == FILTER_TYPE_NUMBER then
return tostring( oper.value )
elseif oper.type == FILTER_TYPE_MOMENT then
return "+" .. tostring( math.max( 0, oper.value - epoch ) ) .. "s"
elseif oper.type == FILTER_TYPE_PERIOD then
return tostring( math.abs( oper.value ) ) .. "s"
elseif oper.type == FILTER_TYPE_SERIES then
return "(" .. string.gsub( table.concat( oper.value, "," ), "[^,]+", "\"%1\"" ) .. ")"
end
end
local function get_escaped_fields( username )
local fields = { }
local vars = get_record_vars( username )
base_filter.add_preset_vars( vars )
for i = 1 + results_horz, #columns_list do
local oper = base_filter.translate( columns_list[ i ], vars )
table.insert( fields, minetest.formspec_escape(
oper and oper.type == FILTER_TYPE_STRING and oper.value or "?" )
)
end
return fields
end
local function sort_results( )
local cache = { }
local field = vars_list[ var_index ]
local select_record = auth_db.select_record
for i, v in ipairs( results_list ) do
local rec = select_record( v )
if rec then
cache[ v ] = ( field == "username" and v or field == "assigned_privs" and #rec[ field ] or rec[ field ] )
end
end
table.sort( results_list, function ( a, b )
local value1, value2 = cache[ a ], cache[ b ]
-- deleted records are lowest sort order
if not value1 then return false end
if not value2 then return true end
if is_sort_reverse then
return value1 > value2
else
return value1 < value2
end
end )
result_index = 1
results_vert = 0
end
local function get_formspec( err )
local fs = minetest.formspec_escape
local horz = ( #columns_list > 1 and ( 1000 / ( #columns_list - 1 ) * results_horz ) or 0 )
local vert = ( #results_list > 1 and ( 1000 / ( #results_list - 1 ) * results_vert ) or 0 )
local formspec = "size[13.5,9.0]"
.. default.gui_bg
.. default.gui_bg_img
.. "label[0.1,0.0;Results (" .. #results_list .. " Records Selected):]"
.. "checkbox[6.5,-0.2;is_sort_reverse;Reverse Sort;" .. tostring( is_sort_reverse ) .. "]"
.. "tablecolumns[color" .. string.rep( ";text,width=10", #columns_list - results_horz ) .. "]"
.. "table[0.1,0.5;8.6,7.3;results_list;#66DD66"
for i = 1 + results_horz, #columns_list do
formspec = formspec .. "," .. fs( string.sub( columns_list[ i ], 1, 18 ) )
end
for i = 1 + results_vert, math.min( #results_list, 15 + results_vert ) do
formspec = formspec .. ",#FFFFFF," .. table.concat( get_escaped_fields( results_list[ i ] ), "," )
end
formspec = formspec .. ";" .. result_index .. "]"
.. "scrollbar[0.1,7.8;8.6,0.4;horizontal;results_horz;" .. horz .. "]"
.. "scrollbar[8.7,0.5;0.37,7.2;vertical;results_vert;" .. vert .. "]"
if err then
formspec = formspec .. "box[0.1,8.4;7.8,0.7;#555555]"
.. "label[0.3,8.5;" .. minetest.colorize( "#CCCC22", "ERROR: " ) .. fs( err ) .. "]"
.. "button[8.1,8.3;1.2,1;okay;Okay]"
else
formspec = formspec .. "dropdown[0.1,8.4;2.4,1;var_index;" .. table.concat( vars_list, "," ) .. ";" .. var_index .. "]"
.. "field[2.8,9.0;3.7,0.25;var_input;;" .. fs( var_input ) .. "]"
.. "button[6.1,8.3;1,1;set_records;Set]"
.. "button[7.0,8.3;1,1;del_records;Del]"
.. "button[8.1,8.3;1.2,1;sort_records;Sort]"
end
formspec = formspec .. "label[9.4,0.0;Columns:]"
.. "textlist[9.4,0.5;2.9,2.7;columns_list"
for i, v in ipairs( columns_list ) do
formspec = formspec .. ( i == 1 and ";" or "," ) .. fs( v )
end
formspec = formspec .. ";" .. column_index .. ";false]"
.. "button[12.4,0.4;1,1;prev_column;<<]"
.. "button[12.4,1.2;1,1;next_column;>>]"
.. "button[12.4,2.0;1,1;del_column;Del]"
.. "button[12.4,3.2;1,1;add_column;Add]"
.. "field[9.7,3.9;3.1,0.25;column_macro;;" .. fs( column_macro ) .. "]"
.. "label[9.4,4.6;Selectors:]"
.. "textlist[9.4,5.1;3.8,2.3;selects_list"
for i, v in ipairs( selects_list ) do
formspec = formspec .. ( i == 1 and ";" or "," ) .. fs( v.input )
end
formspec = formspec .. ";" .. select_index .. ";false]"
.. "field[9.7,8.1;4.0,0.25;select_input;;" .. fs( select_input ) .. "]"
.. "button[9.4,8.3;1.4,1;reset_results;Clear]"
.. "button[12.0,8.3;1.4,1;query_results;Query]"
return formspec
end
local function on_close( meta, player, fields )
-- check single-operation elements first
if fields.okay then
minetest.update_form( name, get_formspec( ) )
elseif fields.is_sort_reverse then
is_sort_reverse = ( fields.is_sort_reverse == "true" )
elseif fields.columns_list then
local event = minetest.explode_textlist_event( fields.columns_list )
if event.type == "CHG" then
column_index = event.index
elseif event.type == "DCL" then
column_macro = columns_list[ column_index ]
minetest.update_form( name, get_formspec( ) )
end
elseif fields.selects_list then
local event = minetest.explode_textlist_event( fields.selects_list )
if event.type == "CHG" then
select_index = event.index
results_list = selects_list[ event.index ].cache
results_vert = 0
minetest.update_form( name, get_formspec( ) )
elseif event.type == "DCL" and select_index > 1 then
select_input = selects_list[ event.index ].input
minetest.update_form( name, get_formspec( ) )
end
elseif fields.results_list then
local event = minetest.explode_table_event( fields.results_list )
if event.type == "CHG" then
result_index = event.row
elseif event.type == "DCL" and result_index > 1 then
local vars = get_record_vars( results_list[ results_vert + result_index - 1 ] )
local oper = vars[ vars_list[ var_index ] ]
var_input = oper and format_value( oper ) or ""
minetest.update_form( name, get_formspec( ) )
end
elseif fields.next_column or fields.prev_column then
local idx = column_index
local off = fields.next_column and 1 or -1
if off == 1 and idx < #columns_list or off == -1 and idx > 1 then
local v = columns_list[ idx ]
columns_list[ idx ] = columns_list[ idx + off ]
columns_list[ idx + off ] = v
column_index = idx + off
minetest.update_form( name, get_formspec( ) )
end
elseif fields.del_column then
if #columns_list > 1 then
table.remove( columns_list, column_index )
column_index = math.min( column_index, #columns_list )
results_horz = 0
minetest.update_form( name, get_formspec( ) )
end
elseif fields.add_column and fields.column_macro then
if string.match( fields.column_macro, "%S+" ) and #columns_list < 10 then
table.insert( columns_list, string.trim( fields.column_macro ) )
column_macro = ""
column_index = #columns_list
minetest.update_form( name, get_formspec( ) )
end
elseif fields.del_records then
local delete_record = auth_db.delete_record
if result_index == 1 then
for i, username in ipairs( results_list ) do
delete_record( username )
end
else
delete_record( results_list[ results_vert + result_index - 1 ] )
end
minetest.update_form( name, get_formspec( ) )
elseif fields.sort_records then
sort_results( )
minetest.update_form( name, get_formspec( ) )
elseif fields.query_results and fields.select_input then
if string.match( fields.select_input, "%S+" ) and #selects_list < 5 then
local input = string.trim( fields.select_input )
local err = query_results( input )
select_input = ( not err and "" or input )
minetest.update_form( name, get_formspec( err ) )
end
elseif fields.reset_results then
reset_results( )
select_input = ""
minetest.update_form( name, get_formspec( ) )
-- check dual-operation elements last
elseif fields.results_horz and fields.results_vert then
local horz_event = minetest.explode_scrollbar_event( fields.results_horz )
local vert_event = minetest.explode_scrollbar_event( fields.results_vert )
if horz_event.type == "CHG" then
local offset = horz_event.value - 1000 / ( #columns_list - 1 ) * results_horz
if offset > 10 then
results_horz = #columns_list - 1
elseif offset < -10 then
results_horz = 0
elseif offset > 0 then
results_horz = results_horz + 1
elseif offset < 0 then
results_horz = results_horz - 1
end
minetest.update_form( name, get_formspec( ) )
elseif vert_event.type == "CHG" then
-- TODO: Fix offset calculation to be more accurate?
local offset = vert_event.value - 1000 / ( #results_list - 1 ) * results_vert
if offset > 10 then
results_vert = math.min( #results_list - 1, results_vert + 100 )
elseif offset < -10 then
results_vert = math.max( 0, results_vert - 100 )
elseif offset > 0 then
results_vert = math.min( #results_list - 1, results_vert + 10 )
elseif offset < 0 then
results_vert = math.max( 0, results_vert - 10 )
end
result_index = 1
minetest.update_form( name, get_formspec( ) )
end
var_index = ( { ["username"] = 1, ["password"] = 2, ["oldlogin"] = 3, ["newlogin"] = 4, ["lifetime"] = 5, ["total_sessions"] = 6, ["total_failures"] = 7, ["total_attempts"] = 8, ["assigned_privs"] = 9 } )[ fields.var_index ] or 1 -- sanity check
end
end
reset_results( )
minetest.create_form( nil, name, get_formspec( ), on_close )
end,
} )
return function ( import )
auth_db = import.auth_db
auth_filter = import.auth_filter

View File

@ -1,5 +1,5 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.12 (auth_rx)
-- Minetest :: Auth Redux Mod v2.13 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
@ -34,20 +34,6 @@ FILTER_COMP_HAS = 57
local decode_base64 = minetest.decode_base64
local encode_base64 = minetest.encode_base64
local trim = function ( str )
return string.sub( str, 2, -2 )
end
local localtime = function ( str )
-- daylight saving time is factored in automatically
local x = { string.match( str, "^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)Z$" ) }
return #x > 0 and os.time( { year = x[ 1 ], month = x[ 2 ], day = x[ 3 ], hour = x[ 4 ], min = x[ 5 ], sec = x[ 6 ] } ) or nil
end
local redate = function ( ts )
-- convert to standard time (for timespec and datespec comparisons)
local x = os.date( "*t", ts )
x.isdst = false
return os.time( x )
end
----------------------------
-- StringPattern class
@ -110,13 +96,12 @@ function NumberPattern( phrase, is_mode, tokens, parser )
end
----------------------------
-- AuthFilter class
-- GenericFilter class
----------------------------
function AuthFilter( path, name, debug )
local src
local is_active = true
function GenericFilter( )
local self = { }
local trim, localtime, redate
local funcs = {
["add"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a + b end },
@ -148,31 +133,21 @@ function AuthFilter( path, name, debug )
["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 },
}
local do_math = { [FILTER_TYPE_NUMBER] = true, [FILTER_TYPE_PERIOD] = true, [FILTER_TYPE_MOMENT] = true, [FILTER_TYPE_DATESPEC] = true, [FILTER_TYPE_TIMESPEC] = true }
local periods = { y = 31536000, w = 604800, d = 86400, h = 3600, m = 60, s = 1 }
----------------------------
-- private methods
----------------------------
local trace, get_operand, get_result, evaluate, tokenize
trace = debug or function ( msg, num )
minetest.log( "error", string.format( "%s (%s/%s, line %d)", msg, path, name, num ) )
return num, "The server encountered an internal error."
end
function get_operand( token, vars )
local t, v, ref
local find_token = function ( pat )
-- use back-references for easier conditional branching
ref = { string.match( token, pat ) }
return #ref > 0 and #ref
end
if find_token( "^(.-)([a-zA-Z0-9_]+)&([A-Za-z0-9+/]*);$" ) then
local name = ref[ 2 ]
local suffix = decode_base64( ref[ 3 ] )
local prefix = ref[ 1 ]
local parsers = {
{ expr = "^%$([a-zA-Z0-9_]+)$", proc = function ( refs, vars )
local name = refs[ 1 ]
if not vars[ name ] or vars[ name ].value == nil then
return nil
end
return { type = vars[ name ].type, value = vars[ name ].value, const = false }
end },
{ expr = "^(.-)([a-zA-Z0-9_]+)&([A-Za-z0-9+/]*);$", proc = function ( refs, vars )
local name = refs[ 2 ]
local suffix = decode_base64( refs[ 3 ] )
local prefix = refs[ 1 ]
suffix = string.gsub( suffix, "%b()", function( str )
-- encode nested function arguments
return "&" .. encode_base64( trim( str ) ) .. ";"
@ -188,52 +163,57 @@ function AuthFilter( path, name, debug )
return nil
end
local params = { }
local c = true
for i, a in ipairs( args ) do
local oper = get_operand( a, vars )
local oper, ix, rx = self.get_operand( a, vars )
if not oper or oper.type ~= funcs[ name ].args[ i ] then
return nil
end
if not oper.const then
-- propagate non-constant to parent
c = false
end
table.insert( params, oper.value )
end
t = funcs[ name ].type
v = funcs[ name ].def( vars, unpack( params ) )
elseif find_token( "^&([A-Za-z0-9+/]*);$" ) then
t = FILTER_TYPE_SERIES
v = { }
local suffix = decode_base64( ref[ 1 ] )
return { type = funcs[ name ].type, value = funcs[ name ].def( vars, unpack( params ) ), const = c }
end },
{ expr = "^&([A-Za-z0-9+/]*);$", proc = function ( refs, vars )
local suffix = decode_base64( refs[ 1 ] )
suffix = string.gsub( suffix, "%b()", function( str )
-- encode nested function arguments
return "&" .. encode_base64( trim( str ) ) .. ";"
end )
local elems = string.split( suffix, ",", false )
for i, e in ipairs( elems ) do
local oper = get_operand( e, vars )
local v = { }
local c = true
for i, a in ipairs( elems ) do
local oper = self.get_operand( a, vars )
if not oper or oper.type ~= FILTER_TYPE_STRING then
return nil
end
if not oper.const then
-- propagate non-constant to parent
c = false
end
table.insert( v, oper.value )
end
elseif find_token( "^%$([a-zA-Z0-9_]+)$" ) then
local name = ref[ 1 ]
if not vars[ name ] or vars[ name ].value == nil then
return nil
end
t = vars[ name ].type
v = vars[ name ].value
elseif find_token( "^@([a-zA-Z0-9_]+%.txt)$" ) then
t = FILTER_TYPE_SERIES
v = { }
local file = io.open( path .. "/filters/" .. ref[ 1 ], "rb" )
return { type = FILTER_TYPE_SERIES, value = v, const = c }
end },
{ expr = "^@([a-zA-Z0-9_]+%.txt)$", proc = function ( refs, vars )
local v = { }
local file = io.open( path .. "/filters/" .. refs[ 1 ], "rb" )
if not file then
return nil
end
for line in file:lines( ) do
table.insert( v, line )
end
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
return { type = FILTER_TYPE_SERIES, value = v, const = true }
end },
{ expr = "^/([a-zA-Z0-9+/]*),([stda]);$", proc = function( refs, vars )
local v
local phrase = minetest.decode_base64( refs[ 1 ] )
if refs[ 2 ] == "s" then
v = StringPattern( phrase, { [FILTER_TYPE_STRING] = true }, {
["["] = "",
["]"] = "",
@ -253,21 +233,21 @@ function AuthFilter( path, name, debug )
["#"] = "%d",
["&"] = "%a",
} )
elseif ref[ 2 ] == "t" then
elseif refs[ 2 ] == "t" then
phrase = string.split( phrase, ":", false )
v = NumberPattern( phrase, { [FILTER_TYPE_MOMENT] = true }, { "%d?%d", "%d%d", "%d%d" }, function ( value )
-- direct translation (accounts for daylight saving time and time-zone offset)
local timespec = os.date( "*t", value )
return { timespec.hour, timespec.min, timespec.sec }
end )
elseif ref[ 2 ] == "d" then
elseif refs[ 2 ] == "d" then
phrase = string.split( phrase, "-", false )
v = NumberPattern( phrase, { [FILTER_TYPE_MOMENT] = true }, { "%d%d", "%d%d", "%d%d%d%d" }, function ( value )
-- direct translation (accounts for daylight saving time and time-zone offset)
local datespec = os.date( "*t", value )
return { datespec.day, datespec.month, datespec.year }
end )
elseif ref[ 2 ] == "a" then
elseif refs[ 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 )
@ -276,80 +256,141 @@ function AuthFilter( path, name, debug )
if not v then
return nil
end
elseif find_token( "^(%d+)([ywdhms])$" ) 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+)([ywdhms])$" ) 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 vars.epoch.value
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
return { type = FILTER_TYPE_PATTERN, value = v, const = true }
end },
{ expr = "^(%d+)([ywdhms])$", proc = function ( refs, vars )
local v = tonumber( refs[ 1 ] ) * periods[ refs[ 2 ] ]
return { type = FILTER_TYPE_PERIOD, value = v, const = true }
end },
{ expr = "^([-+]%d+)([ywdhms])$", proc = function ( refs, vars )
local origin = string.byte( refs[ 1 ] ) == 45 and vars.clock.value or vars.epoch.value
local v = origin + tonumber( refs[ 1 ] ) * periods[ refs[ 2 ] ]
return { type = FILTER_TYPE_MOMENT, value = v, const = true }
end },
{ expr = "^(%d?%d):(%d%d):(%d%d)$", proc = function ( refs, vars )
local timespec = {
isdst = false, day = 1, month = 1, year = 1970, hour = tonumber( ref[ 1 ] ), min = tonumber( ref[ 2 ] ), sec = ref[ 3 ] and tonumber( ref[ 3 ] ) or 0,
isdst = false, day = 1, month = 1, year = 1970, hour = tonumber( refs[ 1 ] ), min = tonumber( refs[ 2 ] ), sec = tonumber( refs[ 3 ] ),
}
t = FILTER_TYPE_TIMESPEC
v = ( os.time( timespec ) - vars.epoch.value ) % 86400 -- strip date component and time-zone offset (standardize time and account for overflow too)
elseif find_token( "^(%d%d)%-(%d%d)%-(%d%d%d%d)$" ) then
-- strip date component and time-zone offset (standardize time and account for overflow too)
local v = ( os.time( timespec ) - vars.epoch.value ) % 86400
return { type = FILTER_TYPE_TIMESPEC, value = v, const = true }
end },
{ expr = "^(%d%d)%-(%d%d)%-(%d%d%d%d)$", proc = function ( refs, vars )
local datespec = {
isdst = false, day = tonumber( ref[ 1 ] ), month = tonumber( ref[ 2 ] ), year = tonumber( ref[ 3 ] ), hour = 0,
isdst = false, day = tonumber( refs[ 1 ] ), month = tonumber( refs[ 2 ] ), year = tonumber( refs[ 3 ] ), hour = 0,
}
t = FILTER_TYPE_DATESPEC
v = math.floor( ( os.time( datespec ) - vars.epoch.value ) / 86400 ) -- strip time component and time-zone offset (standardize time too)
elseif find_token( "^'([a-zA-Z0-9+/]*);$" ) then
t = FILTER_TYPE_STRING
v = decode_base64( ref[ 1 ] )
elseif find_token( "^\"([a-zA-Z0-9+/]*);$" ) then
t = FILTER_TYPE_STRING
v = decode_base64( ref[ 1 ] )
v = string.gsub( v, "%$([a-zA-Z_]+)", function ( var )
return vars[ var ] and tostring( vars[ var ].value ) or "?"
-- strip time component and time-zone offset (standardize time too)
local v = math.floor( ( os.time( datespec ) - vars.epoch.value ) / 86400 )
return { type = FILTER_TYPE_DATESPEC, value = v, const = true }
end },
{ expr = "^'([a-zA-Z0-9+/]*);$", proc = function ( refs, vars )
local v = decode_base64( refs[ 1 ] )
return { type = FILTER_TYPE_STRING, value = v, const = true }
end },
{ expr = "^\"([a-zA-Z0-9+/]*);$", proc = function ( refs, vars )
local v = decode_base64( refs[ 1 ] )
local c = true
v = string.gsub( v, "%$([a-zA-Z_]+)", function ( name )
-- variable interpolation is non-constant
c = false
return vars[ name ] and tostring( vars[ name ].value ) or "?"
end )
elseif find_token( "^-?%d+$" ) or find_token( "^-?%d*%.%d+$" ) then
t = FILTER_TYPE_NUMBER
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
return { type = t, value = v }
return { type = FILTER_TYPE_STRING, value = v, const = c }
end },
{ expr = "^-?%d+$", proc = function ( refs, vars )
local v = tonumber( refs[ 1 ] )
return { type = FILTER_TYPE_NUMBER, value = v, const = true }
end },
{ expr = "^-?%d*%.%d+$", proc = function ( refs, vars )
local v = tonumber( refs[ 1 ] )
return { type = FILTER_TYPE_NUMBER, value = v, const = true }
end },
{ expr = "^(%d+)%.(%d+)%.(%d+)%.(%d+)$", proc = function ( refs, vars )
local v = tonumber( refs[ 1 ] ) * 16777216 + tonumber( refs[ 2 ] ) * 65536 + tonumber( refs[ 3 ] ) * 256 + tonumber( refs[ 4 ] )
return { type = FILTER_TYPE_ADDRESS, value = v, const = true }
end },
}
---- private methods ----
function trim( str )
return string.sub( str, 2, -2 )
end
function get_result( cond, comp, oper1, oper2 )
-- only allow comparisons of appropriate and equivalent datatypes
local do_math = { [FILTER_TYPE_NUMBER] = true, [FILTER_TYPE_PERIOD] = true, [FILTER_TYPE_MOMENT] = true, [FILTER_TYPE_DATESPEC] = true, [FILTER_TYPE_TIMESPEC] = true }
function localtime( str )
-- daylight saving time is factored in automatically
local x = { string.match( str, "^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)Z$" ) }
return #x > 0 and os.time( { year = x[ 1 ], month = x[ 2 ], day = x[ 3 ], hour = x[ 4 ], min = x[ 5 ], sec = x[ 6 ] } ) or nil
end
function redate( ts )
-- convert to standard time (for timespec and datespec comparisons)
local x = os.date( "*t", ts )
x.isdst = false
return os.time( x )
end
---- public methods ----
self.define_func = function ( name, type, args, def )
funcs[ name ] = { type = type, args = args, def = def }
end
self.add_preset_vars = function ( vars )
vars["clock"] = { type = FILTER_TYPE_MOMENT, value = os.time( ) }
vars["epoch"] = { type = FILTER_TYPE_MOMENT, value = os.time( { year = 1970, month = 1, day = 1, hour = 0 } ) }
vars["true"] = { type = FILTER_TYPE_BOOLEAN, value = true }
vars["false"] = { type = FILTER_TYPE_BOOLEAN, value = false }
end
self.get_operand_parser = function ( token )
local match = string.match
for i, v in ipairs( parsers ) do
local refs = { match( token, v.expr ) }
if #refs > 0 then
return v.proc, refs
end
end
end
self.get_operand = function ( token, vars )
local proc, refs = self.get_operand_parser( token )
if proc then return proc( refs, vars ) end
end
self.translate = function ( input, vars )
return self.get_operand( self.tokenize( input ), vars )
end
self.get_result = function ( cond, comp, oper1, oper2 )
local type1 = oper1.type
local type2 = oper2.type
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
-- only allow comparisons of appropriate and equivalent datatypes
if comp == FILTER_COMP_EQ and type1 == type2 and type1 ~= FILTER_TYPE_SERIES and type1 ~= 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
elseif comp == FILTER_COMP_GT and type1 == type2 and do_math[ type2 ] then
expr = ( oper1.value > oper2.value )
elseif comp == FILTER_COMP_GTE and oper1.type == oper2.type and do_math[ oper2.type ] then
elseif comp == FILTER_COMP_GTE and type1 == type2 and do_math[ type2 ] then
expr = ( oper1.value >= oper2.value )
elseif comp == FILTER_COMP_LT and oper1.type == oper2.type and do_math[ oper2.type ] then
elseif comp == FILTER_COMP_LT and type1 == type2 and do_math[ type2 ] then
expr = ( oper1.value < oper2.value )
elseif comp == FILTER_COMP_LTE and oper1.type == oper2.type and do_math[ oper2.type ] then
elseif comp == FILTER_COMP_LTE and type1 == type2 and do_math[ type2 ] then
expr = ( oper1.value <= oper2.value )
elseif comp == FILTER_COMP_IS and oper1.type == FILTER_TYPE_STRING and oper2.type == FILTER_TYPE_STRING then
elseif comp == FILTER_COMP_IS and type1 == FILTER_TYPE_STRING and type2 == FILTER_TYPE_STRING then
expr = ( string.upper( oper1.value ) == string.upper( oper2.value ) )
elseif comp == FILTER_COMP_IN and oper1.type == FILTER_TYPE_STRING and oper2.type == FILTER_TYPE_SERIES then
local value1 = oper1.value
expr = false
for i, value2 in ipairs( oper2.value ) do
expr = ( value1 == value2 )
if expr then break end
end
elseif comp == FILTER_COMP_HAS and oper1.type == FILTER_TYPE_SERIES and oper2.type == FILTER_TYPE_STRING then
elseif comp == FILTER_COMP_IS and type2 == FILTER_TYPE_PATTERN then
expr = oper2.value.compare( oper1.value, type1 )
if expr == nil then return end
elseif comp == FILTER_COMP_HAS and type1 == FILTER_TYPE_SERIES and type2 == FILTER_TYPE_STRING then
local value2 = string.upper( oper2.value )
expr = false
for i, value1 in ipairs( oper1.value ) do
expr = ( string.upper( value1 ) == value2 )
if expr then break end
end
elseif comp == FILTER_COMP_HAS and oper1.type == FILTER_TYPE_SERIES and oper2.type == FILTER_TYPE_PATTERN then
elseif comp == FILTER_COMP_HAS and type1 == FILTER_TYPE_SERIES and type2 == FILTER_TYPE_PATTERN then
local compare = oper2.value.compare
expr = false
for i, value1 in ipairs( oper1.value ) do
@ -357,9 +398,13 @@ function AuthFilter( path, name, debug )
if expr == nil then return end
if expr then break end
end
elseif comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_PATTERN then
expr = oper2.value.compare( oper1.value, oper1.type )
if expr == nil then return end
elseif comp == FILTER_COMP_IN and type1 == FILTER_TYPE_STRING and type2 == FILTER_TYPE_SERIES then
local value1 = oper1.value
expr = false
for i, value2 in ipairs( oper2.value ) do
expr = ( value1 == value2 )
if expr then break end
end
else
return
end
@ -368,7 +413,53 @@ function AuthFilter( path, name, debug )
return expr
end
function evaluate( rule )
self.tokenize = function ( line )
-- encode string and pattern literals and function arguments to simplify parsing (order IS significant)
line = string.gsub( line, "\"(.-)\"", function ( str )
return "\"" .. encode_base64( str ) .. ";"
end )
line = string.gsub( line, "'(.-)'", function ( str )
return "'" .. encode_base64( str ) .. ";"
end )
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 )
return "&" .. encode_base64( trim( str ) ) .. ";"
end )
return line
end
return self
end
----------------------------
-- AuthFilter subclass
----------------------------
function AuthFilter( path, name, debug )
local self = { }
local parent = GenericFilter( ) -- inherit from parent class
local src
local mode_defs = { ["pass"] = FILTER_MODE_PASS, ["fail"] = FILTER_MODE_FAIL }
local bool_defs = { ["all"] = FILTER_BOOL_AND, ["any"] = FILTER_BOOL_OR, ["one"] = FILTER_BOOL_XOR, ["now"] = FILTER_BOOL_NOW }
local cond1_defs = { ["when"] = FILTER_COND_TRUE, ["until"] = FILTER_COND_FALSE }
local cond2_defs = { ["if"] = FILTER_COND_TRUE, ["unless"] = FILTER_COND_FALSE }
local comp_defs = { ["in"] = FILTER_COMP_IN, ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["gte"] = FILTER_COMP_GTE, ["lte"] = FILTER_COMP_LTE, ["has"] = FILTER_COMP_HAS, ["is"] = FILTER_COMP_IS }
---- private methods ----
local get_operand = parent.get_operand
local get_result = parent.get_result
local tokenize = parent.tokenize
local trace = debug or function ( msg, num )
minetest.log( "error", string.format( "%s (%s/%s, line %d)", msg, path, name, num ) )
return "The server encountered an internal error.", num
end
local evaluate = function ( rule )
-- short circuit binary logic to simplify evaluation
local res = ( rule.bool == FILTER_BOOL_AND )
local xor = 0
@ -387,30 +478,9 @@ function AuthFilter( path, name, debug )
return res
end
function tokenize( line )
-- encode string and pattern literals and function arguments to simplify parsing (order IS significant)
line = string.gsub( line, "\"(.-)\"", function ( str )
return "\"" .. encode_base64( str ) .. ";"
end )
line = string.gsub( line, "'(.-)'", function ( str )
return "'" .. encode_base64( str ) .. ";"
end )
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 )
return "&" .. encode_base64( trim( str ) ) .. ";"
end )
return line
end
---- public methods ----
----------------------------
-- public methods
----------------------------
self.translate = function ( field, vars )
return get_operand( tokenize( field ), vars )
end
self.add_preset_vars = parent.add_preset_vars
self.refresh = function ( )
local file = io.open( path .. "/" .. name, "r" )
@ -420,27 +490,16 @@ function AuthFilter( path, name, debug )
src = { }
for line in file:lines( ) do
-- skip comments (lines beginning with hash character) and blank lines
-- TODO: remove extraneous white space at beginning of lines
table.insert( src, string.byte( line ) ~= 35 and tokenize( line ) or "" )
end
file:close( file )
end
self.add_preset_vars = function ( vars )
vars[ "clock" ] = { type = FILTER_TYPE_MOMENT, value = os.time( ) }
vars[ "epoch" ] = { type = FILTER_TYPE_MOMENT, value = os.time( { year = 1970, month = 1, day = 1, hour = 0 } ) }
vars[ "true" ] = { type = FILTER_TYPE_BOOLEAN, value = true }
vars[ "false" ] = { type = FILTER_TYPE_BOOLEAN, value = false }
end
self.process = function( vars )
self.process = function( vars, is_local )
local rule
local note = "Access denied."
if not is_active then return end
if not debug then
-- allow overriding preset vars when debugger is active
if is_local then
self.add_preset_vars( vars )
end
@ -455,7 +514,7 @@ function AuthFilter( path, name, debug )
if #stmt ~= 1 then return trace( "Invalid 'continue' statement in ruleset", num ) end
if evaluate( rule ) then
return num, ( rule.mode == FILTER_MODE_FAIL and note or nil )
return ( rule.mode == FILTER_MODE_FAIL and note or nil ), num
end
rule = nil
@ -475,30 +534,26 @@ function AuthFilter( path, name, debug )
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 = { }
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 ] ]
local mode = mode_defs[ stmt[ 1 ] ]
local bool = bool_defs[ stmt[ 2 ] ]
if not mode or not bool then
return trace( "Unrecognized keywords in ruleset", num )
end
if bool == FILTER_BOOL_NOW then
return num, ( mode == FILTER_MODE_FAIL and note or nil )
return ( mode == FILTER_MODE_FAIL and note or nil ), num
end
rule.mode = mode
rule.bool = bool
rule.expr = { }
rule = { mode = mode, bool = bool, expr = { } }
elseif stmt[ 1 ] == "when" or stmt[ 1 ] == "until" then
if rule then return trace( "Unexpected 'when' or 'until' statement in ruleset", num ) end
if #stmt ~= 5 then return trace( "Invalid 'when' or 'until' statement in ruleset", num ) end
local mode = ( { ["pass"] = FILTER_MODE_PASS, ["fail"] = FILTER_MODE_FAIL } )[ stmt[ 5 ] ]
local cond = ( { ["when"] = FILTER_COND_TRUE, ["until"] = FILTER_COND_FALSE } )[ stmt[ 1 ] ]
local comp = ( { ["in"] = FILTER_COMP_IN, ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["gte"] = FILTER_COMP_GTE, ["lte"] = FILTER_COMP_LTE, ["has"] = FILTER_COMP_HAS, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
local cond = cond1_defs[ stmt[ 1 ] ]
local comp = comp_defs[ stmt[ 3 ] ]
local mode = mode_defs[ stmt[ 5 ] ]
if not cond or not comp then
return trace( "Unrecognized keywords in ruleset", num )
@ -515,15 +570,15 @@ function AuthFilter( path, name, debug )
if expr == nil then
return trace( "Mismatched operands in ruleset", num )
elseif expr then
return num, ( mode == FILTER_MODE_FAIL and note or nil )
return ( mode == FILTER_MODE_FAIL and note or nil ), num
end
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 ] ]
local comp = ( { ["in"] = FILTER_COMP_IN, ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["gte"] = FILTER_COMP_GTE, ["lte"] = FILTER_COMP_LTE, ["has"] = FILTER_COMP_HAS, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
local cond = cond2_defs[ stmt[ 1 ] ]
local comp = comp_defs[ stmt[ 3 ] ]
if not cond or not comp then
return trace( "Unrecognized keywords in ruleset", num )
@ -549,18 +604,6 @@ function AuthFilter( path, name, debug )
return trace( "Unexpected end-of-file in ruleset", 0 )
end
self.enable = function ( )
is_active = true
end
self.disable = function ( )
is_active = false
end
self.is_active = function ( )
return is_active
end
self.refresh( )
return self

View File

@ -1,22 +1,25 @@
--------------------------------------------------------
-- Minetest :: Auth Redux Mod v2.10 (auth_rx)
-- Minetest :: Auth Redux Mod v2.13 (auth_rx)
--
-- See README.txt for licensing and release notes.
-- Copyright (c) 2017-2018, Leslie E. Krause
--------------------------------------------------------
dofile( minetest.get_modpath( "auth_rx" ) .. "/helpers.lua" )
dofile( minetest.get_modpath( "auth_rx" ) .. "/filter.lua" )
dofile( minetest.get_modpath( "auth_rx" ) .. "/db.lua" )
dofile( minetest.get_modpath( "auth_rx" ) .. "/watchdog.lua" )
local __commands = dofile( minetest.get_modpath( "auth_rx" ) .. "/commands.lua" )
local world_path = minetest.get_worldpath( )
local mod_path = minetest.get_modpath( "auth_rx" )
dofile( mod_path .. "/helpers.lua" )
dofile( mod_path .. "/filter.lua" )
dofile( mod_path .. "/db.lua" )
dofile( mod_path .. "/watchdog.lua" )
local __commands = dofile( mod_path .. "/commands.lua" )
-----------------------------------------------------
-- Registered Authentication Handler
-----------------------------------------------------
local auth_filter = AuthFilter( minetest.get_worldpath( ), "greenlist.mt" )
local auth_db = AuthDatabase( minetest.get_worldpath( ), "auth.db" )
local auth_filter = AuthFilter( world_path, "greenlist.mt" )
local auth_db = AuthDatabase( world_path, "auth.db" )
local auth_watchdog = AuthWatchdog( )
if minetest.register_on_auth_fail then
@ -42,7 +45,7 @@ minetest.register_on_prejoinplayer( function ( player_name, player_ip )
end
end
local num, res = auth_filter.process( {
local res = auth_filter.is_enabled and auth_filter.process( {
name = { type = FILTER_TYPE_STRING, value = player_name },
addr = { type = FILTER_TYPE_ADDRESS, value = convert_ipv4( player_ip ) },
is_new = { type = FILTER_TYPE_BOOLEAN, value = rec == nil },
@ -64,7 +67,7 @@ minetest.register_on_prejoinplayer( function ( player_name, player_ip )
ip_newcheck = { type = FILTER_TYPE_MOMENT, value = meta.newcheck or 0 },
ip_failures = { type = FILTER_TYPE_NUMBER, value = meta.count_failures or 0 },
ip_attempts = { type = FILTER_TYPE_NUMBER, value = meta.count_attempts or 0 }
} )
}, true ) or nil
auth_watchdog.on_attempt( convert_ipv4( player_ip ), player_name )
@ -73,12 +76,10 @@ 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" )
local player_ip = minetest.get_player_information( player_name ).address -- this doesn't work in singleplayer!
auth_db.on_login_success( player_name, player_ip )
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 )
auth_watchdog.on_success( convert_ipv4( player_ip ) )
end )
minetest.register_on_leaveplayer( function ( player )
@ -139,5 +140,7 @@ minetest.register_authentication_handler( {
} )
auth_db.connect( )
auth_filter.is_enabled = true
__commands( { auth_db = auth_db, auth_filter = auth_filter } )