add47bd537
- fixed minor typos in code samples - tweaked pattern matching behavior in rulesets
313 lines
10 KiB
Lua
313 lines
10 KiB
Lua
--------------------------------------------------------
|
|
-- Minetest :: Auth Redux Mod v2.3 (auth_rx)
|
|
--
|
|
-- See README.txt for licensing and release notes.
|
|
-- Copyright (c) 2017-2018, Leslie E. Krause
|
|
--
|
|
-- ./games/minetest_game/mods/auth_rx/filter.lua
|
|
--------------------------------------------------------
|
|
|
|
FILTER_TYPE_STRING = 11
|
|
FILTER_TYPE_BOOLEAN = 12
|
|
FILTER_TYPE_NUMBER = 13
|
|
FILTER_TYPE_PATTERN = 14
|
|
FILTER_TYPE_SERIES = 15
|
|
FILTER_MODE_FAIL = 20
|
|
FILTER_MODE_PASS = 21
|
|
FILTER_BOOL_AND = 30
|
|
FILTER_BOOL_OR = 31
|
|
FILTER_BOOL_XOR = 32
|
|
FILTER_BOOL_NOW = 33
|
|
FILTER_COND_FALSE = 40
|
|
FILTER_COND_TRUE = 41
|
|
FILTER_COMP_EQ = 50
|
|
FILTER_COMP_GT = 51
|
|
FILTER_COMP_GTE = 52
|
|
FILTER_COMP_LT = 53
|
|
FILTER_COMP_LTE = 54
|
|
FILTER_COMP_IS = 55
|
|
|
|
----------------------------
|
|
-- AuthFilter class
|
|
----------------------------
|
|
|
|
function AuthFilter( path, name )
|
|
local src = { }
|
|
local opt = { is_debug = false, is_strict = true }
|
|
local self = { }
|
|
|
|
local file = io.open( path .. "/" .. name, "rb" )
|
|
if not file then
|
|
error( "The specified ruleset file does not exist." )
|
|
end
|
|
|
|
for line in file:lines( ) do
|
|
-- encode string and pattern literals to make parsing easier
|
|
line = string.gsub( line, "\"(.-)\"", function ( str )
|
|
return "\"" .. minetest.encode_base64( str )
|
|
end )
|
|
line = string.gsub( line, "'(.-)'", function ( str )
|
|
return "'" .. minetest.encode_base64( str )
|
|
end )
|
|
line = string.gsub( line, "/(.-)/", function ( str )
|
|
return "/" .. minetest.encode_base64( str )
|
|
end )
|
|
|
|
table.insert( src, line )
|
|
end
|
|
|
|
file:close( file )
|
|
|
|
----------------------------
|
|
-- private methods
|
|
----------------------------
|
|
|
|
local trace = function ( msg, num )
|
|
-- TODO: Use 'pcall' for more graceful exception handling?
|
|
minetest.log( "error", string.format( "%s (%s/%s, line %d)", msg, path, name, num ) )
|
|
return "The server encountered an internal error."
|
|
end
|
|
|
|
local get_operand = function ( token, vars )
|
|
local t, v
|
|
|
|
if string.find( token, "^%$[a-zA-Z_]+$" ) then
|
|
local var = string.sub( token, 2 )
|
|
if not vars[ var ] then
|
|
return nil
|
|
else
|
|
t = vars[ var ].type
|
|
v = vars[ var ].value
|
|
end
|
|
elseif string.find( token, "^@[a-zA-Z0-9_]*%.txt$" ) then
|
|
t = FILTER_TYPE_SERIES
|
|
v = { }
|
|
local file = io.open( path .. "/filters/" .. string.sub( token, 2 ), "rb" )
|
|
if not file then
|
|
return nil
|
|
end
|
|
for line in file:lines( ) do
|
|
table.insert( v, line )
|
|
end
|
|
elseif string.find( token, "^/.*$" ) then
|
|
-- sanitize search phrase and convert to regexp pattern
|
|
local sanitizer =
|
|
{
|
|
["["] = "",
|
|
["]"] = "",
|
|
["^"] = "%^",
|
|
["$"] = "%$",
|
|
["("] = "%(",
|
|
[")"] = "%)",
|
|
["%"] = "%%",
|
|
["-"] = "%-",
|
|
[","] = "[a-z]",
|
|
[";"] = "[A-Z]",
|
|
["="] = "[-_]",
|
|
["!"] = "[a-zA-Z0-9]",
|
|
["*"] = "[a-zA-Z0-9_-]*",
|
|
["+"] = "[a-zA-Z0-9_-]+",
|
|
["?"] = "[a-zA-Z0-9_-]",
|
|
["#"] = "%d",
|
|
["&"] = "%a",
|
|
}
|
|
t = FILTER_TYPE_PATTERN
|
|
v = minetest.decode_base64( string.sub( token, 2 ) )
|
|
v = "^" .. string.gsub( v, ".", sanitizer ) .. "$"
|
|
elseif string.find( token, "^'.*$" ) then
|
|
t = FILTER_TYPE_STRING
|
|
v = minetest.decode_base64( string.sub( token, 2 ) )
|
|
elseif string.find( token, "^\".*$" ) then
|
|
t = FILTER_TYPE_STRING
|
|
v = minetest.decode_base64( string.sub( token, 2 ) )
|
|
v = string.gsub( v, "%$([a-zA-Z_]+)", function ( var )
|
|
return vars[ var ] and tostring( vars[ var ].value ) or "?"
|
|
end )
|
|
elseif string.find( token, "^-?%d+$" ) or string.find( token, "^-?%d*%.%d+$" ) then
|
|
t = FILTER_TYPE_NUMBER
|
|
v = tonumber( token )
|
|
else
|
|
return nil
|
|
end
|
|
return { type = t, value = v }
|
|
end
|
|
|
|
local evaluate = function ( rule )
|
|
-- short circuit binary logic to simplify evaluation
|
|
local res = ( rule.bool == FILTER_BOOL_AND )
|
|
local xor = 0
|
|
|
|
for i, v in ipairs( rule.expr ) do
|
|
if rule.bool == FILTER_BOOL_AND and not v then
|
|
return false
|
|
elseif rule.bool == FILTER_BOOL_OR and v then
|
|
return true
|
|
elseif rule.bool == FILTER_BOOL_XOR and v then
|
|
xor = xor + 1
|
|
end
|
|
end
|
|
if xor == 1 then return true end
|
|
|
|
return res
|
|
end
|
|
|
|
----------------------------
|
|
-- public methods
|
|
----------------------------
|
|
|
|
self.process = function( vars )
|
|
local rule
|
|
local note = "Access denied."
|
|
|
|
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( ) }
|
|
|
|
for num, line in ipairs( src ) do
|
|
|
|
-- FIXME: ignore extraneous whitespace, even at beginning of line
|
|
local stmt = string.split( line, " ", false )
|
|
|
|
if string.byte( line ) == 35 or #stmt == 0 then
|
|
-- skip comments (lines beginning with hash character) and empty lines
|
|
-- TODO: these should be stripped on file import
|
|
|
|
elseif stmt[ 1 ] == "continue" then
|
|
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
|
|
|
|
rule = nil
|
|
|
|
elseif stmt[ 1 ] == "try" then
|
|
if rule then return trace( "Missing 'continue' statement in ruleset", num ) end
|
|
if #stmt ~= 2 then return trace( "Invalid 'try' statement in ruleset", num ) end
|
|
|
|
local oper = get_operand( stmt[ 2 ], vars )
|
|
if not oper or oper.type ~= FILTER_TYPE_STRING then
|
|
return trace( "Unrecognized operand in ruleset", num )
|
|
end
|
|
|
|
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 #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 ] ]
|
|
|
|
if not mode or not bool then
|
|
return trace( "Unrecognized keywords in ruleset", num )
|
|
end
|
|
|
|
if bool == FILTER_BOOL_NOW then
|
|
return ( mode == FILTER_MODE_FAIL and note or nil )
|
|
end
|
|
|
|
rule.mode = mode
|
|
rule.bool = bool
|
|
rule.expr = { }
|
|
|
|
elseif stmt[ 1 ] == "when" or stmt[ 1 ] == "until" then
|
|
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 ] ]
|
|
local comp = ( { ["eq"] = FILTER_COMP_EQ, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
|
|
|
|
if not cond or not comp then
|
|
return trace( "Unrecognized keywords in ruleset", num )
|
|
end
|
|
|
|
local oper1 = get_operand( stmt[ 2 ], vars )
|
|
local oper2 = get_operand( stmt[ 4 ], vars )
|
|
|
|
if not oper1 or not oper2 then
|
|
return trace( "Unrecognized operands in ruleset", num )
|
|
elseif oper1.type ~= FILTER_TYPE_SERIES then
|
|
return trace( "Mismatched operands in ruleset", num )
|
|
end
|
|
|
|
-- cache second operand value for efficiency
|
|
-- TODO: might want to move the redundant operand type checks out of loop?
|
|
|
|
local value2 = ( comp == FILTER_COMP_IS and oper2.type == FILTER_TYPE_STRING ) and string.upper( oper2.value ) or oper2.value
|
|
local type2 = oper2.type
|
|
local expr = false
|
|
|
|
for i, value1 in ipairs( oper1.value ) do
|
|
if comp == FILTER_COMP_EQ and type2 == FILTER_TYPE_STRING then
|
|
expr = ( value1 == value2 )
|
|
elseif comp == FILTER_COMP_IS and type2 == FILTER_TYPE_STRING then
|
|
expr = ( string.upper( value1 ) == value2 )
|
|
elseif comp == FILTER_COMP_IS and type2 == FILTER_TYPE_PATTERN then
|
|
expr = ( string.find( value1, value2 ) == 1 )
|
|
else
|
|
return trace( "Mismatched operands in ruleset", num )
|
|
end
|
|
if expr then break end
|
|
end
|
|
if cond == FILTER_COND_FALSE then expr = not expr end
|
|
|
|
table.insert( rule.expr, expr )
|
|
|
|
elseif stmt[ 1 ] == "if" or stmt[ 1 ] == "unless" then
|
|
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 = ( { ["eq"] = FILTER_COMP_EQ, ["gt"] = FILTER_COMP_GT, ["lt"] = FILTER_COMP_LT, ["gte"] = FILTER_COMP_GTE, ["lte"] = FILTER_COMP_LTE, ["is"] = FILTER_COMP_IS } )[ stmt[ 3 ] ]
|
|
|
|
if not cond or not comp then
|
|
return trace( "Unrecognized keywords in ruleset", num )
|
|
end
|
|
|
|
local oper1 = get_operand( stmt[ 2 ], vars )
|
|
local oper2 = get_operand( stmt[ 4 ], vars )
|
|
|
|
if not oper1 or not oper2 then
|
|
return trace( "Unrecognized operands in ruleset", num )
|
|
end
|
|
|
|
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_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
|
|
if cond == FILTER_COND_FALSE then expr = not expr end
|
|
|
|
table.insert( rule.expr, expr )
|
|
|
|
-- TODO: immediately evaluating each expression (thus avoiding a list) would be optimal,
|
|
-- but probably requires state table; efficiency vs complexity scenario
|
|
|
|
else
|
|
return trace( "Invalid statement in ruleset", num )
|
|
end
|
|
end
|
|
return trace( "Unexpected end-of-file in ruleset", 0 )
|
|
end
|
|
|
|
return self
|
|
end
|