added authentication mod for subnasa huge manage of users as first step
* using https://codeberg.org/minenux/minetest-mod-formspecs * using https://codeberg.org/minenux/minetest-mod-auth_rx * changes: ** provide a way to initialize files if there is not one currently doe snot touch the auth.txt file neither converted ** solves: close: https://codeberg.org/minenux/minetest-mod-auth_rx/issues/6 ** solved: close: https://bitbucket.org/sorcerykid/auth_rx/issues/7 ** init the files when are fresh install, still do not convert from auth.txt ** player object check for problematic joins on inpcomplete auth process ** close fixed https://codeberg.org/minenux/minetest-mod-auth_rx/issues/2 ** added missing depends formspecs (it work without in basics but, some commands needs) ** we will later aded formspecs checks to made optional
This commit is contained in:
parent
f5c992c054
commit
cfb83aab12
@ -8,6 +8,8 @@ For further information, check https://codeberg.org/minenux/minetest-game-subna
|
||||
* Informacion español: [GAMEINFO-es.md](GAMEINFO-es.md), **ADVERTENCIA: si se cuestiona porque no esta el mod "x" en vez de el mod "y" lease el archivo de informacion en la seccion inicial**
|
||||
* Информация русском: [GAMEINFO-ru.md](GAMEINFO-ru.md), **ВНИМАНИЕ: если у вас возник вопрос, почему нет мода "x" вместо мода "y" прочитайте информационный файл в начальном разделе**
|
||||
|
||||
![screenshot.png](screenshot.png)
|
||||
|
||||
## Installation
|
||||
|
||||
For specific minetest installation check [docs/INSTALL.md](docs/INSTALL.md)
|
||||
@ -33,6 +35,12 @@ To download you can play this game with the following minetest engines:
|
||||
* https://github.com/minenux/minetest-engine-luk3yx/tree/stable-4.1
|
||||
* https://github.com/MultiCraft/MultiCraft-legacy/releases/tag/v1.10.0
|
||||
|
||||
#### Mods
|
||||
|
||||
* minetest default
|
||||
* minetest Auth Redux as `auth_rx` [mods/auth_rx](mods/auth_rx) from https://codeberg.org/minenux/minetest-mod-auth_rx
|
||||
** so then minetest Formspecs as `formspecs` [mods/formspecs](mods/formspecs) from https://codeberg.org/minenux/minetest-mod-formspecs
|
||||
|
||||
## Licensing
|
||||
|
||||
See `LICENSE.txt`
|
||||
|
159
mods/auth_rx/README.txt
Normal file
159
mods/auth_rx/README.txt
Normal file
@ -0,0 +1,159 @@
|
||||
Auth Redux Mod v2.13
|
||||
By Leslie Krause
|
||||
|
||||
Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
|
||||
It is designed from the ground up to be robust and secure enough for use on high-traffic
|
||||
Minetest servers, while also addressing a number of outstanding bugs (including #5334
|
||||
and #6783 and #4451) in the Minetest 0.4.x engine.
|
||||
|
||||
Auth Redux is intended to be compatible with all versions of Minetest 0.4.14+.
|
||||
|
||||
https://forum.minetest.net/viewtopic.php?f=9&t=20393
|
||||
|
||||
Repository
|
||||
----------------------
|
||||
|
||||
Browse source code:
|
||||
https://bitbucket.org/sorcerykid/auth_rx
|
||||
|
||||
Download archive:
|
||||
https://bitbucket.org/sorcerykid/auth_rx/get/master.zip
|
||||
https://bitbucket.org/sorcerykid/auth_rx/get/master.tar.gz
|
||||
|
||||
Revision History
|
||||
----------------------
|
||||
|
||||
Version 2.1b (30-Jun-2018)
|
||||
- initial beta version
|
||||
- included code samples for basic login filtering
|
||||
- included a command-line database import script
|
||||
|
||||
Version 2.2b (04-Jul-2018)
|
||||
- added install option to database import script
|
||||
- improved exception handling by AuthFilter class
|
||||
- fixed parsing of number literals in rulesets
|
||||
- fixed type-checking of try statements in rulesets
|
||||
- included mod.conf and description.txt files
|
||||
|
||||
Version 2.3b (08-Jul-2018)
|
||||
- general code cleanup of AuthFilter class
|
||||
- moved datasets into separate directory of world
|
||||
- added two more comparison operators for rulesets
|
||||
- tweaked pattern matching behavior in rulesets
|
||||
- changed database search method to use Lua regexes
|
||||
- removed hard-coded file names from database methods
|
||||
|
||||
Version 2.4b (13-Jul-2018)
|
||||
- moved Journal and AuthDatabase classes into library
|
||||
- added rollback function to AuthDatabase class
|
||||
- reworked journal audit to support rollback option
|
||||
- better encapsulated database commit function
|
||||
- allowed for STOPPED opcode during database update
|
||||
- various changes to error and action messages
|
||||
- moved command-line scripts to separate directory
|
||||
- included script to rollback database via journal
|
||||
- included script to extract debug log into journal
|
||||
|
||||
Version 2.5b (17-Jul-2018)
|
||||
- implemented function parsing algorithm for rulesets
|
||||
- simplified operand matching logic in rulesets
|
||||
- improved transcoding of literals in rulesets
|
||||
- added some basic functions for use by rulesets
|
||||
- fixed validation of dataset names in rulesets
|
||||
|
||||
Version 2.6b (19-Jul-2018)
|
||||
- introduced support for array literals in rulesets
|
||||
- added array-related functions for use by rulesets
|
||||
- localized references to transcoding functions
|
||||
- registered chat command to control login filtering
|
||||
- included support for disabling login filtering
|
||||
- added reload function to AuthFilter class
|
||||
- tweaked lexer to skip comments on ruleset loading
|
||||
- added search function to AuthDatabase class
|
||||
|
||||
Version 2.7b (22-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
|
||||
|
||||
Version 2.8b (24-Jul-2018)
|
||||
- introduced support for numeric patterns in rulesets
|
||||
- created polymorphic pattern-matching classes
|
||||
- excluded time-zone offsets in time/date comparisons
|
||||
- standardized timestamps for use in filter functions
|
||||
- removed daylight saving time from preset variables
|
||||
- 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
|
||||
|
||||
Version 2.10b (29-Jul-2018)
|
||||
- major code reorganization via multiple libraries
|
||||
- removed extra variables from AuthFilter class
|
||||
- developed interactive debugger for testing rulesets
|
||||
- added optional debugger hooks in AuthFilter class
|
||||
- allowed for overriding preset variables by debugger
|
||||
- included line-number in results of login filter
|
||||
- added missing preset variable needed by rulesets
|
||||
|
||||
Version 2.11 (04-Aug-2018)
|
||||
- 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
|
||||
----------------------
|
||||
|
||||
1) Unzip the archive into the mods directory of your game
|
||||
2) Rename the auth_rx-master directory to "auth_rx"
|
||||
3) Execute the "convert.awk" script (refer to instructions)
|
||||
|
||||
Source Code License
|
||||
----------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2018, Leslie Krause (leslie@searstower.org)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more details:
|
||||
https://opensource.org/licenses/MIT
|
741
mods/auth_rx/commands.lua
Normal file
741
mods/auth_rx/commands.lua
Normal file
@ -0,0 +1,741 @@
|
||||
--------------------------------------------------------
|
||||
-- Minetest :: Auth Redux Mod v2.13 (auth_rx)
|
||||
--
|
||||
-- See README.txt for licensing and release notes.
|
||||
-- Copyright (c) 2017-2018, Leslie E. Krause
|
||||
--------------------------------------------------------
|
||||
|
||||
local auth_db, auth_filter -- imported
|
||||
|
||||
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_enabled and "enabled" or "disabled" ) .. "."
|
||||
elseif param == "disable" then
|
||||
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.is_enabled = true
|
||||
minetest.log( "action", "Login filtering enabled by " .. name .. "." )
|
||||
return true, "Login filtering is enabled."
|
||||
elseif param == "reload" then
|
||||
auth_filter.refresh( )
|
||||
return true, "Ruleset definition was loaded successfully."
|
||||
else
|
||||
return false, "Unknown parameter specified."
|
||||
end
|
||||
end
|
||||
} )
|
||||
|
||||
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
|
||||
|
||||
local epoch = os.time( { year = 1970, month = 1, day = 1, hour = 0 } )
|
||||
local vars = {
|
||||
__debug = { type = FILTER_TYPE_NUMBER, value = 0 },
|
||||
name = { type = FILTER_TYPE_STRING, value = "singleplayer" },
|
||||
addr = { type = FILTER_TYPE_ADDRESS, value = convert_ipv4( "127.0.0.1" ) },
|
||||
is_new = { type = FILTER_TYPE_BOOLEAN, value = true },
|
||||
privs_list = { type = FILTER_TYPE_SERIES, value = { } },
|
||||
users_list = { type = FILTER_TYPE_SERIES, is_auto = true },
|
||||
cur_users = { type = FILTER_TYPE_NUMBER, is_auto = true },
|
||||
max_users = { type = FILTER_TYPE_NUMBER, value = get_minetest_config( "max_users" ) },
|
||||
lifetime = { type = FILTER_TYPE_PERIOD, value = 0 },
|
||||
sessions = { type = FILTER_TYPE_NUMBER, value = 0 },
|
||||
failures = { type = FILTER_TYPE_NUMBER, value = 0 },
|
||||
attempts = { type = FILTER_TYPE_NUMBER, value = 0 },
|
||||
owner = { type = FILTER_TYPE_STRING, value = get_minetest_config( "name" ) },
|
||||
uptime = { type = FILTER_TYPE_PERIOD, is_auto = true },
|
||||
oldlogin = { type = FILTER_TYPE_MOMENT, value = epoch },
|
||||
newlogin = { type = FILTER_TYPE_MOMENT, value = epoch },
|
||||
ip_names_list = { type = FILTER_TYPE_SERIES, value = { } },
|
||||
ip_prelogin = { type = FILTER_TYPE_MOMENT, value = epoch },
|
||||
ip_oldcheck = { type = FILTER_TYPE_MOMENT, value = epoch },
|
||||
ip_newcheck = { type = FILTER_TYPE_MOMENT, value = epoch },
|
||||
ip_failures = { type = FILTER_TYPE_NUMBER, value = 0 },
|
||||
ip_attempts = { type = FILTER_TYPE_NUMBER, value = 0 }
|
||||
}
|
||||
local vars_list = { "__debug", "clock", "name", "addr", "is_new", "privs_list", "users_list", "cur_users", "max_users", "lifetime", "sessions", "failures", "attempts", "owner", "uptime", "oldlogin", "newlogin", "ip_names_list", "ip_prelogin", "ip_oldcheck", "ip_newcheck", "ip_failures", "ip_attempts" }
|
||||
local datatypes = { [FILTER_TYPE_NUMBER] = "NUMBER", [FILTER_TYPE_STRING] = "STRING", [FILTER_TYPE_BOOLEAN] = "BOOLEAN", [FILTER_TYPE_ADDRESS] = "ADDRESS", [FILTER_TYPE_PERIOD] = "PERIOD", [FILTER_TYPE_MOMENT] = "MOMENT", [FILTER_TYPE_SERIES] = "SERIES" }
|
||||
local has_prompt = true
|
||||
local has_output = true
|
||||
local login_index = 2
|
||||
local var_index = 1
|
||||
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 )
|
||||
-- clear debug prompts from source code
|
||||
return string.gsub( buffer, "\n# ====== .- ======\n", "\n", has_single and 1 or nil )
|
||||
end
|
||||
local function insert_prompt( buffer, num, err )
|
||||
-- insert debug prompts into source code
|
||||
local i = 0
|
||||
return string.gsub( buffer, "\n", function ( )
|
||||
i = i + 1
|
||||
return ( i == num and string.format( "\n# ====== ^ Line %d: %s ^ ======\n", num, err ) or "\n" )
|
||||
end )
|
||||
end
|
||||
local function format_value( value, type )
|
||||
-- convert values to a human-readable format
|
||||
if type == FILTER_TYPE_STRING then
|
||||
return "\"" .. value .. "\""
|
||||
elseif type == FILTER_TYPE_NUMBER then
|
||||
return tostring( value )
|
||||
elseif type == FILTER_TYPE_BOOLEAN then
|
||||
return "$" .. tostring( value )
|
||||
elseif type == FILTER_TYPE_PERIOD then
|
||||
return tostring( math.abs( value ) ) .. "s"
|
||||
elseif type == FILTER_TYPE_MOMENT then
|
||||
return "+" .. tostring( value - vars.epoch.value ) .. "s"
|
||||
elseif type == FILTER_TYPE_ADDRESS then
|
||||
return table.concat( unpack_address( value ), "." )
|
||||
elseif type == FILTER_TYPE_SERIES then
|
||||
return "(" .. string.gsub( table.concat( value, "," ), "[^,]+", "\"%1\"" ) .. ")"
|
||||
end
|
||||
end
|
||||
local function update_vars( )
|
||||
-- automatically update preset variables
|
||||
if vars.uptime.is_auto then
|
||||
vars.uptime.value = minetest.get_server_uptime( ) end
|
||||
if vars.clock.is_auto then
|
||||
vars.clock.value = os.time( ) end
|
||||
if vars.users_list.is_auto then
|
||||
vars.users_list.value = auth_db.search( true ) end
|
||||
if vars.cur_users.is_auto then
|
||||
vars.cur_users.value = #auth_db.search( true ) end
|
||||
end
|
||||
local function get_formspec( buffer, status, var_state )
|
||||
local var_name = vars_list[ var_index ]
|
||||
local var_type = vars[ var_name ].type
|
||||
local var_value = vars[ var_name ].value
|
||||
local var_is_auto = vars[ var_name ].is_auto
|
||||
|
||||
local formspec = "size[13.5,8.5]"
|
||||
.. default.gui_bg
|
||||
.. default.gui_bg_img
|
||||
.. "label[0.1,0.0;Ruleset Definition:]"
|
||||
.. "checkbox[2.6,-0.2;has_output;Show Client Output;" .. tostring( has_output ) .. "]"
|
||||
.. "checkbox[5.6,-0.2;has_prompt;Show Debug Prompt;" .. tostring( has_prompt ) .. "]"
|
||||
.. "textarea[0.4,0.5;8.6," .. ( not status and "8.4" or status.user and "5.6" or "7.3" ) .. ";buffer;;" .. minetest.formspec_escape( buffer ) .. "]"
|
||||
.. "button[0.1,7.8;2,1;export_ruleset;Save]"
|
||||
.. "button[2.0,7.8;2,1;import_ruleset;Load]"
|
||||
.. "button[4.0,7.8;2,1;process_ruleset;Process]"
|
||||
.. "dropdown[6,7.9;2.6,1;login_mode;Normal,New Account,Wrong Password;" .. login_index .. "]"
|
||||
|
||||
.. "label[9.0,0.0;Preset Variables:]"
|
||||
.. "textlist[9.0,0.5;4,4.7;vars_list"
|
||||
|
||||
for i, v in pairs( vars_list ) do
|
||||
formspec = formspec .. ( i == 1 and ";" or "," ) .. minetest.formspec_escape( v .. " = " .. format_value( vars[ v ].value, vars[ v ].type ) )
|
||||
end
|
||||
formspec = formspec .. string.format( ";%d;false]", var_index )
|
||||
.. "label[9.0,5.4;Name:]"
|
||||
.. "label[9.0,5.9;Type:]"
|
||||
.. string.format( "label[10.5,5.4;%s]", minetest.colorize( "#BBFF77", "$" .. var_name ) )
|
||||
.. string.format( "label[10.5,5.9;%s]", datatypes[ var_type ] )
|
||||
.. "label[9.0,6.4;Value:]"
|
||||
.. "field[9.2,7.5;4.3,0.25;var_value;;" .. minetest.formspec_escape( format_value( var_value, var_type ) ) .. "]"
|
||||
.. "button[9.0,7.8;1,1;prev_var;<<]"
|
||||
.. "button[10.0,7.8;1,1;next_var;>>]"
|
||||
.. "button[11.8,7.8;1.5,1;set_var;Set]"
|
||||
|
||||
if var_is_auto ~= nil then
|
||||
formspec = formspec .. "checkbox[10.5,6.2;var_is_auto;Auto Update;" .. tostring( var_is_auto ) .. "]"
|
||||
end
|
||||
|
||||
if status then
|
||||
formspec = formspec .. "box[0.1,6.9;8.4,0.8;#555555]"
|
||||
.. "label[0.3,7.1;" .. minetest.colorize( status.type == "ERROR" and "#CCCC22" or "#22CC22", status.type .. ": " ) .. status.desc .. "]"
|
||||
if status.user then
|
||||
formspec = formspec .. "textlist[0.1,5.5;8.4,1.2;;Access denied. Reason: " .. minetest.formspec_escape( status.user ) .. ";0;false]"
|
||||
end
|
||||
end
|
||||
return formspec
|
||||
end
|
||||
local function on_close( meta, player, fields )
|
||||
login_index = ( { ["Normal"] = 1, ["New Account"] = 2, ["Wrong Password"] = 3 } )[ fields.login_mode ] or 1 -- sanity check
|
||||
|
||||
if fields.quit then
|
||||
os.remove( minetest.get_worldpath( ) .. "/~greenlist.mt" )
|
||||
|
||||
elseif fields.vars_list then
|
||||
local event = minetest.explode_textlist_event( fields.vars_list )
|
||||
if event.type == "CHG" then
|
||||
var_index = event.index
|
||||
minetest.update_form( name, get_formspec( fields.buffer ) )
|
||||
end
|
||||
|
||||
elseif fields.has_prompt then
|
||||
has_prompt = fields.has_prompt == "true"
|
||||
|
||||
elseif fields.has_output then
|
||||
has_output = fields.has_output == "true"
|
||||
|
||||
elseif fields.export_ruleset then
|
||||
local buffer = clear_prompts( fields.buffer .. "\n", true )
|
||||
local file = io.open( minetest.get_worldpath( ) .. "/greenlist.mt", "w" )
|
||||
if not file then
|
||||
error( "Cannot write to ruleset definition file." )
|
||||
end
|
||||
file:write( buffer )
|
||||
file:close( )
|
||||
minetest.update_form( name, get_formspec( buffer, { type = "ACTION", desc = "Ruleset definition exported." } ) )
|
||||
|
||||
elseif fields.import_ruleset then
|
||||
local file = io.open( minetest.get_worldpath( ) .. "/greenlist.mt", "r" )
|
||||
if not file then
|
||||
error( "Cannot read from ruleset definition file." )
|
||||
end
|
||||
minetest.update_form( name, get_formspec( file:read( "*a" ), { type = "ACTION", desc = "Ruleset definition imported." } ) )
|
||||
file:close( )
|
||||
|
||||
elseif fields.process_ruleset then
|
||||
local status
|
||||
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( ) .. "/" .. temp_name, "w" )
|
||||
temp_file:write( buffer )
|
||||
temp_file:close( )
|
||||
temp_filter.refresh( )
|
||||
|
||||
update_vars( )
|
||||
|
||||
if fields.login_mode == "New Account" then
|
||||
vars.is_new.value = true
|
||||
vars.privs_list.value = { }
|
||||
vars.lifetime.value = 0
|
||||
vars.sessions.value = 0
|
||||
vars.failures.value = 0
|
||||
vars.attempts.value = 0
|
||||
vars.newlogin.value = epoch
|
||||
vars.oldlogin.value = epoch
|
||||
else
|
||||
vars.is_new.value = false
|
||||
vars.attempts.value = vars.attempts.value + 1
|
||||
end
|
||||
|
||||
-- process ruleset and benchmark performance
|
||||
local t = minetest.get_us_time( )
|
||||
local res, num, err = temp_filter.process( vars )
|
||||
t = ( minetest.get_us_time( ) - t ) / 1000
|
||||
|
||||
if err then
|
||||
if has_prompt then buffer = insert_prompt( buffer, num, err ) end
|
||||
status = { type = "ERROR", desc = string.format( "%s (line %d).", err, num ), user = has_output and res }
|
||||
|
||||
vars.ip_attempts.value = vars.ip_attempts.value + 1
|
||||
vars.ip_prelogin.value = vars.clock.value
|
||||
table.insert( vars.ip_names_list.value, vars.name.value )
|
||||
|
||||
elseif res 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 res }
|
||||
|
||||
vars.ip_attempts.value = vars.ip_attempts.value + 1
|
||||
vars.ip_prelogin.value = vars.clock.value
|
||||
table.insert( vars.ip_names_list.value, vars.name.value )
|
||||
|
||||
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 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
|
||||
vars.ip_failures.value = vars.ip_failures.value + 1
|
||||
vars.ip_prelogin.value = vars.clock.value
|
||||
vars.ip_newcheck.value = vars.clock.value
|
||||
if vars.ip_oldcheck.value == epoch then
|
||||
vars.ip_oldcheck.value = vars.clock.value
|
||||
end
|
||||
table.insert( vars.ip_names_list.value, vars.name.value )
|
||||
|
||||
else
|
||||
if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset passed" ) end
|
||||
status = { type = "ACTION", desc = string.format( "Ruleset passed at line %d (took %0.1f ms).", num, t ) }
|
||||
|
||||
if fields.login_mode == "New Account" then
|
||||
vars.privs_list.value = get_default_privs( )
|
||||
end
|
||||
vars.sessions.value = vars.sessions.value + 1
|
||||
vars.newlogin.value = vars.clock.value
|
||||
if vars.oldlogin.value == epoch then
|
||||
vars.oldlogin.value = vars.clock.value
|
||||
end
|
||||
vars.ip_failures.value = 0
|
||||
vars.ip_attempts.value = 0
|
||||
vars.ip_prelogin.value = epoch
|
||||
vars.ip_oldcheck.value = epoch
|
||||
vars.ip_newcheck.value = epoch
|
||||
vars.ip_names_list.value = { }
|
||||
end
|
||||
|
||||
minetest.update_form( name, get_formspec( buffer, status ) )
|
||||
|
||||
elseif fields.next_var or fields.prev_var then
|
||||
local idx = var_index
|
||||
local off = fields.next_var and 1 or -1
|
||||
if off == 1 and idx < #vars_list or off == -1 and idx > 1 then
|
||||
local v = vars_list[ idx ]
|
||||
vars_list[ idx ] = vars_list[ idx + off ]
|
||||
vars_list[ idx + off ] = v
|
||||
var_index = idx + off
|
||||
minetest.update_form( name, get_formspec( fields.buffer ) )
|
||||
end
|
||||
|
||||
elseif fields.var_is_auto then
|
||||
local var_name = vars_list[ var_index ]
|
||||
vars[ var_name ].is_auto = ( fields.var_is_auto == "true" )
|
||||
|
||||
elseif fields.set_var then
|
||||
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
|
||||
-- debug variable can be any value/type
|
||||
vars.__debug = oper
|
||||
elseif oper and oper.type == vars[ var_name ].type then
|
||||
vars[ var_name ].value = oper.value
|
||||
end
|
||||
|
||||
minetest.update_form( name, get_formspec( fields.buffer ) )
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
temp_filter.add_preset_vars( vars )
|
||||
vars.clock.is_auto = true
|
||||
update_vars( )
|
||||
|
||||
minetest.create_form( nil, name, get_formspec( "pass now\n" ), on_close )
|
||||
|
||||
return true
|
||||
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
|
||||
end
|
447
mods/auth_rx/db.lua
Normal file
447
mods/auth_rx/db.lua
Normal file
@ -0,0 +1,447 @@
|
||||
--------------------------------------------------------
|
||||
-- Minetest :: Auth Redux Mod v2.6 (auth_rx)
|
||||
--
|
||||
-- See README.txt for licensing and release notes.
|
||||
-- Copyright (c) 2017-2018, Leslie E. Krause
|
||||
--------------------------------------------------------
|
||||
|
||||
----------------------------
|
||||
-- Transaction Op Codes
|
||||
----------------------------
|
||||
|
||||
local LOG_STARTED = 10 -- <timestamp> 10
|
||||
local LOG_CHECKED = 11 -- <timestamp> 11
|
||||
local LOG_STOPPED = 12 -- <timestamp> 12
|
||||
local TX_CREATE = 20 -- <timestamp> 20 <username> <password>
|
||||
local TX_DELETE = 21 -- <timestamp> 21 <username>
|
||||
local TX_SET_PASSWORD = 40 -- <timestamp> 40 <username> <password>
|
||||
local TX_SET_APPROVED_ADDRS = 41 -- <timestamp> 41 <username> <approved_addrs>
|
||||
local TX_SET_ASSIGNED_PRIVS = 42 -- <timestamp> 42 <username> <assigned_privs>
|
||||
local TX_SESSION_OPENED = 50 -- <timestamp> 50 <username>
|
||||
local TX_SESSION_CLOSED = 51 -- <timestamp> 51 <username>
|
||||
local TX_LOGIN_ATTEMPT = 30 -- <timestamp> 30 <username> <ip>
|
||||
local TX_LOGIN_FAILURE = 31 -- <timestamp> 31 <username> <ip>
|
||||
local TX_LOGIN_SUCCESS = 32 -- <timestamp> 32 <username>
|
||||
|
||||
----------------------------
|
||||
-- Journal Class
|
||||
----------------------------
|
||||
|
||||
function Journal( path, name, is_rollback )
|
||||
local file, err = io.open( path .. "/" .. name, "r+b" )
|
||||
local self = { }
|
||||
local cursor = 0
|
||||
local rtime = 1.0
|
||||
|
||||
-- TODO: Verify integrity of database index
|
||||
if not file then
|
||||
minetest.log( "error", "Cannot open " .. path .. "/" .. name .. " for writing." )
|
||||
error( "Fatal exception in Journal( ), aborting." )
|
||||
end
|
||||
|
||||
self.audit = function ( update_proc, is_rollback )
|
||||
-- Advance to the last set of noncommitted transactions (if any)
|
||||
if not is_rollback then
|
||||
minetest.log( "action", "Advancing database transaction log...." )
|
||||
for line in file:lines( ) do
|
||||
local fields = string.split( line, " ", true )
|
||||
|
||||
if tonumber( fields[ 2 ] ) == LOG_STOPPED then
|
||||
cursor = file:seek( )
|
||||
end
|
||||
end
|
||||
file:seek( "set", cursor )
|
||||
end
|
||||
|
||||
-- Update the database with all noncommitted transactions
|
||||
local meta = { }
|
||||
minetest.log( "action", "Replaying database transaction log...." )
|
||||
for line in file:lines( ) do
|
||||
local fields = string.split( line, " ", true )
|
||||
local optime = tonumber( fields[ 1 ] )
|
||||
local opcode = tonumber( fields[ 2 ] )
|
||||
|
||||
update_proc( meta, optime, opcode, select( 3, unpack( fields ) ) )
|
||||
|
||||
if opcode == LOG_CHECKED then
|
||||
-- Perform the commit and reset the log, if successful
|
||||
minetest.log( "action", "Resetting database transaction log..." )
|
||||
file:seek( "set", cursor )
|
||||
file:write( optime .. " " .. LOG_STOPPED .. "\n" )
|
||||
return optime
|
||||
end
|
||||
cursor = file:seek( )
|
||||
end
|
||||
end
|
||||
self.start = function ( )
|
||||
self.optime = os.time( )
|
||||
file:seek( "end", 0 )
|
||||
file:write( self.optime .. " " .. LOG_STARTED .. "\n" )
|
||||
cursor = file:seek( )
|
||||
file:write( self.optime .. " " .. LOG_CHECKED .. "\n" )
|
||||
end
|
||||
self.reset = function ( )
|
||||
file:seek( "set", cursor )
|
||||
file:write( self.optime .. " " .. LOG_STOPPED .. "\n" )
|
||||
self.optime = nil
|
||||
end
|
||||
self.record_raw = function ( opcode, ... )
|
||||
file:seek( "set", cursor )
|
||||
file:write( table.concat( { self.optime, opcode, ... }, " " ) .. "\n" )
|
||||
cursor = file:seek( )
|
||||
file:write( self.optime .. " " .. LOG_CHECKED .. "\n" )
|
||||
end
|
||||
minetest.register_globalstep( function( dtime )
|
||||
rtime = rtime - dtime
|
||||
if rtime <= 0.0 then
|
||||
if self.optime then
|
||||
-- touch file every 1.0 secs so we know if/when server crashes
|
||||
self.optime = os.time( )
|
||||
file:seek( "set", cursor )
|
||||
file:write( self.optime .. " " .. LOG_CHECKED .. "\n" )
|
||||
end
|
||||
rtime = 1.0
|
||||
end
|
||||
end )
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
----------------------------
|
||||
-- AuthDatabase Class
|
||||
----------------------------
|
||||
|
||||
function AuthDatabase( path, name )
|
||||
local data, users, index
|
||||
local self = { }
|
||||
local journal = Journal( path, name .. "x" )
|
||||
|
||||
-- Private methods
|
||||
|
||||
local db_update = function( meta, optime, opcode, ... )
|
||||
local fields = { ... }
|
||||
|
||||
if opcode == TX_CREATE then
|
||||
local rec =
|
||||
{
|
||||
password = fields[ 2 ],
|
||||
oldlogin = -1,
|
||||
newlogin = -1,
|
||||
lifetime = 0,
|
||||
total_sessions = 0,
|
||||
total_attempts = 0,
|
||||
total_failures = 0,
|
||||
approved_addrs = { },
|
||||
assigned_privs = { },
|
||||
}
|
||||
data[ fields[ 1 ] ] = rec
|
||||
|
||||
elseif opcode == TX_DELETE then
|
||||
data[ fields[ 1 ] ] = nil
|
||||
|
||||
elseif opcode == TX_SET_PASSWORD then
|
||||
data[ fields[ 1 ] ].password = fields[ 2 ]
|
||||
|
||||
elseif opcode == TX_SET_APPROVED_ADDRS then
|
||||
data[ fields[ 1 ] ].filered_addrs = string.split( fields[ 2 ], ",", true )
|
||||
|
||||
elseif opcode == TX_SET_ASSIGNED_PRIVS then
|
||||
data[ fields[ 1 ] ].assigned_privs = string.split( fields[ 2 ], ",", true )
|
||||
|
||||
elseif opcode == TX_LOGIN_ATTEMPT then
|
||||
data[ fields[ 1 ] ].total_attempts = data[ fields[ 1 ] ].total_attempts + 1
|
||||
|
||||
elseif opcode == TX_LOGIN_FAILURE then
|
||||
data[ fields[ 1 ] ].total_failures = data[ fields[ 1 ] ].total_failures + 1
|
||||
|
||||
elseif opcode == TX_LOGIN_SUCCESS then
|
||||
if data[ fields[ 1 ] ].oldlogin == -1 then
|
||||
data[ fields[ 1 ] ].oldlogin = optime
|
||||
end
|
||||
meta.users[ fields[ 1 ] ] = data[ fields[ 1 ] ].newlogin
|
||||
data[ fields[ 1 ] ].newlogin = optime
|
||||
|
||||
elseif opcode == TX_SESSION_OPENED then
|
||||
data[ fields[ 1 ] ].total_sessions = data[ fields[ 1 ] ].total_sessions + 1
|
||||
|
||||
elseif opcode == TX_SESSION_CLOSED then
|
||||
data[ fields[ 1 ] ].lifetime = data[ fields[ 1 ] ].lifetime + ( optime - data[ fields[ 1 ] ].newlogin )
|
||||
meta.users[ fields[ 1 ] ] = nil
|
||||
|
||||
elseif opcode == LOG_STARTED then
|
||||
meta.users = { }
|
||||
|
||||
elseif opcode == LOG_CHECKED or opcode == LOG_STOPPED then
|
||||
-- calculate leftover session lengths due to abnormal server termination
|
||||
for u, t in pairs( meta.users ) do
|
||||
data[ u ].lifetime = data[ u ].lifetime + ( optime - data[ u ].newlogin )
|
||||
end
|
||||
meta.users = nil
|
||||
end
|
||||
end
|
||||
|
||||
local db_reload = function ( )
|
||||
minetest.log( "action", "Reading authentication data from disk..." )
|
||||
|
||||
local file, errmsg = io.open( path .. "/" .. name, "r+b" )
|
||||
if not file then
|
||||
minetest.log( "error", "Cannot open " .. path .. "/" .. name .. " for reading." )
|
||||
error( "Fatal exception in AuthDatabase:db_reload( ), aborting." )
|
||||
end
|
||||
|
||||
local head = assert( file:read( "*line" ) )
|
||||
|
||||
index = tonumber( string.match( head, "^auth_rx/2.1 @(%d+)$" ) )
|
||||
if not index or index < 0 then
|
||||
minetest.log( "error", "Invalid header in authentication database." )
|
||||
error( "Fatal exception in AuthDatabase:reload( ), aborting." )
|
||||
end
|
||||
|
||||
for line in file:lines( ) do
|
||||
if line ~= "" then
|
||||
local fields = string.split( line, ":", true )
|
||||
if #fields ~= 10 then
|
||||
minetest.log( "error", "Invalid record in authentication database." )
|
||||
error( "Fatal exception in AuthDatabase:reload( ), aborting." )
|
||||
end
|
||||
data[ fields[ 1 ] ] = {
|
||||
password = fields[ 2 ],
|
||||
oldlogin = tonumber( fields[ 3 ] ),
|
||||
newlogin = tonumber( fields[ 4 ] ),
|
||||
lifetime = tonumber( fields[ 5 ] ),
|
||||
total_sessions = tonumber( fields[ 6 ] ),
|
||||
total_attempts = tonumber( fields[ 7 ] ),
|
||||
total_failures = tonumber( fields[ 8 ] ),
|
||||
approved_addrs = string.split( fields[ 9 ], "," ),
|
||||
assigned_privs = string.split( fields[ 10 ], "," ),
|
||||
}
|
||||
end
|
||||
end
|
||||
file:close( )
|
||||
end
|
||||
|
||||
local db_commit = function ( )
|
||||
minetest.log( "action", "Writing authentication data to disk..." )
|
||||
|
||||
local file, errmsg = io.open( path .. "/~" .. name, "w+b" )
|
||||
if not file then
|
||||
minetest.log( "error", "Cannot open " .. path .. "/~" .. name .. " for writing." )
|
||||
error( "Fatal exception in AuthDatabase:db_commit( ), aborting." )
|
||||
end
|
||||
|
||||
index = index + 1
|
||||
file:write( "auth_rx/2.1 @" .. index .. "\n" )
|
||||
|
||||
for username, rec in pairs( data ) do
|
||||
assert( file:write( table.concat( {
|
||||
username,
|
||||
rec.password,
|
||||
rec.oldlogin,
|
||||
rec.newlogin,
|
||||
rec.lifetime,
|
||||
rec.total_sessions,
|
||||
rec.total_attempts,
|
||||
rec.total_failures,
|
||||
table.concat( rec.approved_addrs, "," ),
|
||||
table.concat( rec.assigned_privs, "," ),
|
||||
}, ":" ) .. "\n" ) )
|
||||
end
|
||||
file:close( )
|
||||
|
||||
assert( os.remove( path .. "/" .. name ) )
|
||||
assert( os.rename( path .. "/~" .. name, path .. "/" .. name ) )
|
||||
end
|
||||
|
||||
-- Public methods
|
||||
|
||||
self.rollback = function ( )
|
||||
data = { }
|
||||
|
||||
db_reload( )
|
||||
journal.audit( db_update, true )
|
||||
db_commit( )
|
||||
|
||||
data = nil
|
||||
end
|
||||
|
||||
self.connect = function ( )
|
||||
data = { }
|
||||
users = { }
|
||||
|
||||
db_reload( )
|
||||
if journal.audit( db_update, false ) then
|
||||
db_commit( )
|
||||
end
|
||||
journal.start( )
|
||||
end
|
||||
|
||||
self.disconnect = function ( )
|
||||
for u, t in pairs( users ) do
|
||||
data[ u ].lifetime = data[ u ].lifetime + ( journal.optime - data[ u ].newlogin )
|
||||
end
|
||||
|
||||
db_commit( )
|
||||
journal.reset( )
|
||||
|
||||
data = nil
|
||||
users = nil
|
||||
end
|
||||
|
||||
self.create_record = function ( username, password )
|
||||
-- don't allow clobbering existing users
|
||||
if data[ username ] then return false end
|
||||
|
||||
local rec =
|
||||
{
|
||||
password = password,
|
||||
oldlogin = -1,
|
||||
newlogin = -1,
|
||||
lifetime = 0,
|
||||
total_sessions = 0,
|
||||
total_attempts = 0,
|
||||
total_failures = 0,
|
||||
approved_addrs = { },
|
||||
assigned_privs = { },
|
||||
}
|
||||
data[ username ] = rec
|
||||
journal.record_raw( TX_CREATE, username, password )
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
self.delete_record = function ( username )
|
||||
-- don't allow deletion of online users or non-existent users
|
||||
if not data[ username ] or users[ username ] then return false end
|
||||
|
||||
data[ username ] = nil
|
||||
journal.record_raw( TX_DELETE, username )
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
self.set_password = function ( username, password )
|
||||
if not data[ username ] then return false end
|
||||
|
||||
data[ username ].password = password
|
||||
journal.record_raw( TX_SET_PASSWORD, username, password )
|
||||
return true
|
||||
end
|
||||
|
||||
self.set_assigned_privs = function ( username, assigned_privs )
|
||||
if not data[ username ] then return false end
|
||||
|
||||
data[ username ].assigned_privs = assigned_privs
|
||||
journal.record_raw( TX_SET_ASSIGNED_PRIVS, username, table.concat( assigned_privs, "," ) )
|
||||
return true
|
||||
end
|
||||
|
||||
self.set_approved_addrs = function ( username, approved_addrs )
|
||||
if not data[ username ] then return false end
|
||||
|
||||
data[ username ].approved_addrs = approved_addrs
|
||||
journal.record_raw( TX_SET_APPROVED_ADDRS, username, table.concat( approved_addrs, "," ) )
|
||||
return true
|
||||
end
|
||||
|
||||
self.on_session_opened = function ( username )
|
||||
data[ username ].total_sessions = data[ username ].total_sessions + 1
|
||||
journal.record_raw( TX_SESSION_OPENED, username )
|
||||
end
|
||||
|
||||
self.on_session_closed = function ( username )
|
||||
data[ username ].lifetime = data[ username ].lifetime + ( journal.optime - data[ username ].newlogin )
|
||||
users[ username ] = nil
|
||||
journal.record_raw( TX_SESSION_CLOSED, username )
|
||||
end
|
||||
|
||||
self.on_login_attempt = function ( username, ip )
|
||||
data[ username ].total_attempts = data[ username ].total_attempts + 1
|
||||
journal.record_raw( TX_LOGIN_ATTEMPT, username, ip )
|
||||
end
|
||||
|
||||
self.on_login_failure = function ( username, ip )
|
||||
data[ username ].total_failures = data[ username ].total_failures + 1
|
||||
journal.record_raw( TX_LOGIN_FAILURE, username, ip )
|
||||
end
|
||||
|
||||
self.on_login_success = function ( username, ip )
|
||||
if not data[ username ].oldlogin or data[ username ].oldlogin == -1 then
|
||||
data[ username ].oldlogin = journal.optime
|
||||
end
|
||||
users[ username ] = data[ username ].newlogin
|
||||
data[ username ].newlogin = journal.optime
|
||||
journal.record_raw( TX_LOGIN_SUCCESS, username, ip )
|
||||
end
|
||||
|
||||
self.records = function ( )
|
||||
return pairs( data )
|
||||
end
|
||||
|
||||
self.records_match = function ( pattern )
|
||||
local k
|
||||
return function ( )
|
||||
local v
|
||||
local p = string.lower( pattern )
|
||||
|
||||
while true do
|
||||
k, v = next( data, k )
|
||||
if not k then
|
||||
return
|
||||
elseif string.match( string.lower( k ), p ) then
|
||||
return k, v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.select_record = function ( username )
|
||||
return data[ username ]
|
||||
end
|
||||
|
||||
self.search = function ( is_online, pattern )
|
||||
local res = { }
|
||||
local src = is_online and users or data
|
||||
for k, v in pairs( src ) do
|
||||
if pattern == nil or string.match( k, pattern ) then
|
||||
table.insert( res, k )
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
----------------------------
|
||||
-- AuthInitFile Class
|
||||
----------------------------
|
||||
|
||||
function AuthInitFile( path, name)
|
||||
|
||||
local file, err = io.open( path .. "/" .. name, "r+b" )
|
||||
|
||||
-- init empty files if not present
|
||||
if file then
|
||||
file:close( )
|
||||
return
|
||||
end
|
||||
file = io.open( path .. "/" .. name, "w+")
|
||||
if not file then
|
||||
minetest.log( "error", "Cannot init file " .. path .. "/" .. name .. " for writing." )
|
||||
error( "Fatal exception in InitFiles( ), aborting." )
|
||||
end
|
||||
if name == "auth.db" then
|
||||
file:write("auth_rx/2.1 @0")
|
||||
AuthInitFile( path, name .. "x")
|
||||
end
|
||||
file:write("")
|
||||
file:close( )
|
||||
|
||||
-- convert present txt file if present or autentications, after convert, change extension to auth.txt.converted
|
||||
-- TODO: made a conversion in lua line by line, after rename , delete it
|
||||
|
||||
-- provide a way to rollback to a txt file if there already a auth db file, to stay in sync
|
||||
-- TODO way to get sync with txt file
|
||||
|
||||
end
|
2
mods/auth_rx/depends.txt
Normal file
2
mods/auth_rx/depends.txt
Normal file
@ -0,0 +1,2 @@
|
||||
default
|
||||
formspecs
|
1
mods/auth_rx/description.txt
Normal file
1
mods/auth_rx/description.txt
Normal file
@ -0,0 +1 @@
|
||||
Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest. It is designed from the ground up to be robust and secure enough for use on high-traffic Minetest servers, while also addressing a number of outstanding engine bugs.
|
616
mods/auth_rx/filter.lua
Normal file
616
mods/auth_rx/filter.lua
Normal file
@ -0,0 +1,616 @@
|
||||
--------------------------------------------------------
|
||||
-- Minetest :: Auth Redux Mod v2.13 (auth_rx)
|
||||
--
|
||||
-- See README.txt for licensing and release notes.
|
||||
-- Copyright (c) 2017-2018, Leslie E. Krause
|
||||
--------------------------------------------------------
|
||||
|
||||
FILTER_TYPE_STRING = 10
|
||||
FILTER_TYPE_NUMBER = 11
|
||||
FILTER_TYPE_ADDRESS = 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
|
||||
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_IN = 55
|
||||
FILTER_COMP_IS = 56
|
||||
FILTER_COMP_HAS = 57
|
||||
|
||||
local decode_base64 = minetest.decode_base64
|
||||
local encode_base64 = minetest.encode_base64
|
||||
|
||||
----------------------------
|
||||
-- StringPattern class
|
||||
----------------------------
|
||||
|
||||
function StringPattern( phrase, is_mode, tokens )
|
||||
local glob = "^" .. string.gsub( phrase, ".", tokens ) .. "$"
|
||||
return { compare = function ( value, type )
|
||||
if not is_mode[ type ] then return end
|
||||
|
||||
return string.find( value, glob ) == 1
|
||||
end }
|
||||
end
|
||||
|
||||
----------------------------
|
||||
-- NumberPattern class
|
||||
----------------------------
|
||||
|
||||
function NumberPattern( phrase, is_mode, tokens, parser )
|
||||
local glob = { }
|
||||
local ref
|
||||
local find_token = function ( str, pat )
|
||||
ref = { string.match( str, pat ) }
|
||||
return #ref > 0
|
||||
end
|
||||
if #phrase ~= #tokens then
|
||||
return nil
|
||||
end
|
||||
for i, v in ipairs( phrase ) do
|
||||
local eval, args
|
||||
local t = tokens[ i ]
|
||||
if find_token( v, "^(" .. t .. ")$" ) then
|
||||
eval = function ( a, b ) return a == b end
|
||||
args = { tonumber( ref[ 1 ] ) }
|
||||
elseif find_token( v, "^(" .. t .. ")%^(" .. t .. ")$" ) then
|
||||
eval = function ( a, b, c ) return a >= b and a <= c end
|
||||
args = { tonumber( ref[ 1 ] ), tonumber( ref[ 2 ] ) }
|
||||
elseif find_token( v, "^(" .. t .. ")([<>])$" ) then
|
||||
eval = ref[ 2 ] == "<" and
|
||||
( function ( a, b ) return a <= b end ) or
|
||||
( function ( a, b ) return a >= b end )
|
||||
args = { tonumber( ref[ 1 ] ) }
|
||||
elseif v == "?" then
|
||||
eval = function ( ) return true end
|
||||
args = { }
|
||||
else
|
||||
return nil
|
||||
end
|
||||
table.insert( glob, { eval = eval, args = args } )
|
||||
end
|
||||
return { compare = function ( value, type )
|
||||
if not is_mode[ type ] then return end
|
||||
|
||||
local fields = parser( value, type )
|
||||
for i, v in ipairs( glob ) do
|
||||
if not v.eval( fields[ i ], unpack( v.args ) ) then return false end
|
||||
end
|
||||
return true
|
||||
end }
|
||||
end
|
||||
|
||||
----------------------------
|
||||
-- GenericFilter class
|
||||
----------------------------
|
||||
|
||||
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 },
|
||||
["sub"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a - b end },
|
||||
["mul"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a * b end },
|
||||
["div"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return a / b end },
|
||||
["neg"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER }, def = function ( v, a ) return -a end },
|
||||
["abs"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER }, def = function ( v, a ) return math.abs( a ) end },
|
||||
["max"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return math.max( a, b ) end },
|
||||
["min"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER, FILTER_TYPE_NUMBER }, def = function ( v, a, b ) return math.min( a, b ) end },
|
||||
["int"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_NUMBER }, def = function ( v, a ) return a < 0 and math.ceil( a ) or math.floor( a ) end },
|
||||
["num"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return tonumber( a ) or 0 end },
|
||||
["len"] = { type = FILTER_TYPE_NUMBER, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return string.len( a ) end },
|
||||
["lc"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return string.lower( a ) end },
|
||||
["uc"] = { type = FILTER_TYPE_STRING, args = { FILTER_TYPE_STRING }, def = function ( v, a ) return string.upper( a ) end },
|
||||
["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 > 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 },
|
||||
["age"] = { type = FILTER_TYPE_PERIOD, args = { FILTER_TYPE_MOMENT }, def = function ( v, a ) return v.clock.value - a end },
|
||||
["before"] = { type = FILTER_TYPE_MOMENT, args = { FILTER_TYPE_MOMENT, FILTER_TYPE_PERIOD }, def = function ( v, a, b ) return a - b end },
|
||||
["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 },
|
||||
}
|
||||
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 }
|
||||
|
||||
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 ) ) .. ";"
|
||||
end )
|
||||
local args = string.split( suffix, ",", false )
|
||||
if string.match( prefix, "->$" ) then
|
||||
-- insert prefixed arguments
|
||||
table.insert( args, 1, string.sub( prefix, 1, -3 ) )
|
||||
elseif prefix ~= "" then
|
||||
return nil
|
||||
end
|
||||
if not funcs[ name ] or #funcs[ name ].args ~= #args then
|
||||
return nil
|
||||
end
|
||||
local params = { }
|
||||
local c = true
|
||||
for i, a in ipairs( args ) do
|
||||
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
|
||||
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 )
|
||||
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
|
||||
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
|
||||
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 }, {
|
||||
["["] = "",
|
||||
["]"] = "",
|
||||
["^"] = "%^",
|
||||
["$"] = "%$",
|
||||
["("] = "%(",
|
||||
[")"] = "%)",
|
||||
["%"] = "%%",
|
||||
["-"] = "%-",
|
||||
[","] = "[a-z]",
|
||||
[";"] = "[A-Z]",
|
||||
["="] = "[-_]",
|
||||
["!"] = "[a-zA-Z0-9]",
|
||||
["*"] = "[a-zA-Z0-9_-]*",
|
||||
["+"] = "[a-zA-Z0-9_-]+",
|
||||
["?"] = "[a-zA-Z0-9_-]",
|
||||
["#"] = "%d",
|
||||
["&"] = "%a",
|
||||
} )
|
||||
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 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 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 )
|
||||
end )
|
||||
end
|
||||
if not v then
|
||||
return nil
|
||||
end
|
||||
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( refs[ 1 ] ), min = tonumber( refs[ 2 ] ), sec = tonumber( refs[ 3 ] ),
|
||||
}
|
||||
-- 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( refs[ 1 ] ), month = tonumber( refs[ 2 ] ), year = tonumber( refs[ 3 ] ), hour = 0,
|
||||
}
|
||||
-- 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 )
|
||||
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 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
|
||||
|
||||
-- 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 type1 == type2 and do_math[ type2 ] then
|
||||
expr = ( oper1.value > oper2.value )
|
||||
elseif comp == FILTER_COMP_GTE and type1 == type2 and do_math[ type2 ] then
|
||||
expr = ( oper1.value >= oper2.value )
|
||||
elseif comp == FILTER_COMP_LT and type1 == type2 and do_math[ type2 ] then
|
||||
expr = ( oper1.value < oper2.value )
|
||||
elseif comp == FILTER_COMP_LTE and type1 == type2 and do_math[ type2 ] then
|
||||
expr = ( oper1.value <= oper2.value )
|
||||
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_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 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
|
||||
expr = compare( value1, FILTER_TYPE_STRING )
|
||||
if expr == nil then return end
|
||||
if expr then break end
|
||||
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
|
||||
if cond == FILTER_COND_FALSE then expr = not expr end
|
||||
|
||||
return expr
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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.add_preset_vars = parent.add_preset_vars
|
||||
|
||||
self.refresh = function ( )
|
||||
local file = io.open( path .. "/" .. name, "r" )
|
||||
if not file then
|
||||
file = io.open( path .. "/" .. name, "w+")
|
||||
file:write("pass now")
|
||||
if not file then
|
||||
error( "The specified ruleset file does not exist." )
|
||||
end
|
||||
file:close( file )
|
||||
file = io.open( path .. "/" .. name, "r" )
|
||||
end
|
||||
src = { }
|
||||
for line in file:lines( ) do
|
||||
-- skip comments (lines beginning with hash character) and blank lines
|
||||
table.insert( src, string.byte( line ) ~= 35 and tokenize( line ) or "" )
|
||||
end
|
||||
file:close( file )
|
||||
end
|
||||
|
||||
self.process = function( vars, is_local )
|
||||
local rule
|
||||
local note = "Access denied."
|
||||
|
||||
if is_local then
|
||||
self.add_preset_vars( vars )
|
||||
end
|
||||
|
||||
for num, line in ipairs( src ) do
|
||||
local stmt = string.split( line, " ", false )
|
||||
|
||||
if #stmt == 0 then
|
||||
-- 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 evaluate( rule ) then
|
||||
return ( rule.mode == FILTER_MODE_FAIL and note or nil ), num
|
||||
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
|
||||
|
||||
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 ( mode == FILTER_MODE_FAIL and note or nil ), num
|
||||
end
|
||||
|
||||
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 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 )
|
||||
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 = get_result( cond, comp, oper1, oper2 )
|
||||
if expr == nil then
|
||||
return trace( "Mismatched operands in ruleset", num )
|
||||
elseif expr then
|
||||
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 = cond2_defs[ stmt[ 1 ] ]
|
||||
local comp = comp_defs[ 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 = get_result( cond, comp, oper1, oper2 )
|
||||
if expr == nil then
|
||||
return trace( "Mismatched operands in ruleset", num )
|
||||
end
|
||||
|
||||
table.insert( rule.expr, expr )
|
||||
else
|
||||
return trace( "Invalid statement in ruleset", num )
|
||||
end
|
||||
end
|
||||
return trace( "Unexpected end-of-file in ruleset", 0 )
|
||||
end
|
||||
|
||||
self.refresh( )
|
||||
|
||||
return self
|
||||
end
|
45
mods/auth_rx/helpers.lua
Normal file
45
mods/auth_rx/helpers.lua
Normal file
@ -0,0 +1,45 @@
|
||||
--------------------------------------------------------
|
||||
-- Minetest :: Auth Redux Mod v2.10 (auth_rx)
|
||||
--
|
||||
-- See README.txt for licensing and release notes.
|
||||
-- Copyright (c) 2017-2018, Leslie E. Krause
|
||||
--------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------
|
||||
-- Global Helper Functions
|
||||
-----------------------------------------------------
|
||||
|
||||
get_minetest_config = core.setting_get -- backwards compatibility
|
||||
|
||||
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
|
||||
|
||||
function unpack_address( addr )
|
||||
return { math.floor( addr / 16777216 ), math.floor( ( addr % 16777216 ) / 65536 ), math.floor( ( addr % 65536 ) / 256 ), addr % 256 }
|
||||
end
|
||||
|
||||
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
|
||||
return default_privs
|
||||
end
|
||||
|
||||
function unpack_privileges( assigned_privs )
|
||||
local privileges = { }
|
||||
for _, p in ipairs( assigned_privs ) do
|
||||
privileges[ p ] = true
|
||||
end
|
||||
return privileges
|
||||
end
|
||||
|
||||
function pack_privileges( privileges )
|
||||
local assigned_privs = { }
|
||||
for p, _ in pairs( privileges ) do
|
||||
table.insert( assigned_privs, p )
|
||||
end
|
||||
return assigned_privs
|
||||
end
|
167
mods/auth_rx/init.lua
Normal file
167
mods/auth_rx/init.lua
Normal file
@ -0,0 +1,167 @@
|
||||
--------------------------------------------------------
|
||||
-- Minetest :: Auth Redux Mod v2.13 (auth_rx)
|
||||
--
|
||||
-- See README.txt for licensing and release notes.
|
||||
-- Copyright (c) 2017-2018, Leslie E. Krause
|
||||
--------------------------------------------------------
|
||||
|
||||
local world_path = minetest.get_worldpath( )
|
||||
local mod_name = minetest.get_current_modname() or "auth_rx"
|
||||
local mod_path = minetest.get_modpath( mod_name )
|
||||
|
||||
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" )
|
||||
|
||||
AuthInitFile( world_path, "auth.db" )
|
||||
AuthInitFile( world_path, "greenlist.mt" )
|
||||
|
||||
-----------------------------------------------------
|
||||
-- Registered Authentication Handler
|
||||
-----------------------------------------------------
|
||||
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
|
||||
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 meta = auth_watchdog.get_metadata( convert_ipv4( player_ip ) )
|
||||
|
||||
if rec then
|
||||
auth_db.on_login_attempt( 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
|
||||
return string.format( "A player named %s already exists on this server.", cname )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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 },
|
||||
privs_list = { type = FILTER_TYPE_SERIES, value = rec and rec.assigned_privs or { } },
|
||||
users_list = { type = FILTER_TYPE_SERIES, value = auth_db.search( true ) },
|
||||
cur_users = { type = FILTER_TYPE_NUMBER, value = #auth_db.search( true ) },
|
||||
max_users = { type = FILTER_TYPE_NUMBER, value = get_minetest_config( "max_users" ) },
|
||||
lifetime = { type = FILTER_TYPE_PERIOD, value = rec and rec.lifetime or 0 },
|
||||
sessions = { type = FILTER_TYPE_NUMBER, value = rec and rec.total_sessions or 0 },
|
||||
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 },
|
||||
ip_names_list = { type = FILTER_TYPE_SERIES, value = meta.previous_names or { } },
|
||||
ip_prelogin = { type = FILTER_TYPE_MOMENT, value = meta.prelogin or 0 },
|
||||
ip_oldcheck = { type = FILTER_TYPE_MOMENT, value = meta.oldcheck or 0 },
|
||||
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 )
|
||||
|
||||
return res
|
||||
end )
|
||||
|
||||
minetest.register_on_joinplayer( function ( player )
|
||||
local player_name
|
||||
local player_oj
|
||||
local player_ip
|
||||
if player ~= nil then
|
||||
player_name = player:get_player_name( )
|
||||
player_oj = minetest.get_player_information( player_name )
|
||||
player_ip = player_oj.address -- this doesn't work in singleplayer!
|
||||
auth_db.on_login_success( player_name, player_ip )
|
||||
auth_db.on_session_opened( player_name )
|
||||
auth_watchdog.on_success( convert_ipv4( player_ip ) )
|
||||
minetest.log( "warning", "[auth_rx] in authentication database players success joined ".. player_name )
|
||||
else
|
||||
minetest.log( "error", "[auth_rx] incomplete auth process on player obj nil ip obj address" )
|
||||
end
|
||||
end )
|
||||
|
||||
minetest.register_on_leaveplayer( function ( player )
|
||||
|
||||
local name
|
||||
if player ~= nil then
|
||||
name = player:get_player_name( )
|
||||
auth_db.on_session_closed( name )
|
||||
minetest.log( "warning", "[auth_rx] player "..name.." leaving" )
|
||||
else
|
||||
minetest.log( "error", "[auth_rx] incomplete player leaving without writing in dbx due nil object" )
|
||||
end
|
||||
end )
|
||||
|
||||
minetest.register_on_shutdown( function( )
|
||||
auth_db.disconnect( )
|
||||
end )
|
||||
|
||||
minetest.register_authentication_handler( {
|
||||
-- translate old auth hooks to new database backend
|
||||
get_auth = function( username )
|
||||
local rec = auth_db.select_record( username )
|
||||
if rec then
|
||||
local assigned_privs = rec.assigned_privs
|
||||
|
||||
if get_minetest_config( "name" ) == username then
|
||||
-- grant server operator all privileges
|
||||
-- (TODO: implement as function that honors give_to_admin flag)
|
||||
assigned_privs = { }
|
||||
for priv in pairs( core.registered_privileges ) do
|
||||
table.insert( assigned_privs, priv )
|
||||
end
|
||||
end
|
||||
|
||||
return { password = rec.password, privileges = unpack_privileges( assigned_privs ), last_login = rec.newlogin }
|
||||
end
|
||||
end,
|
||||
create_auth = function( username, password )
|
||||
if auth_db.create_record( username, password ) then
|
||||
auth_db.set_assigned_privs( username, get_default_privs( ) )
|
||||
minetest.log( "info", "Created player '" .. username .. "' in authentication database" )
|
||||
end
|
||||
end,
|
||||
delete_auth = function( username )
|
||||
if auth_db.delete_record( username ) then
|
||||
minetest.log( "info", "Deleted player '" .. username .. "' in authenatication database" )
|
||||
end
|
||||
end,
|
||||
set_password = function ( username, password )
|
||||
if auth_db.set_password( username, password ) then
|
||||
minetest.log( "info", "Reset password of player '" .. username .. "' in authentication database" )
|
||||
end
|
||||
end,
|
||||
set_privileges = function ( username, privileges )
|
||||
-- server operator's privileges are immutable
|
||||
if get_minetest_config( "name" ) == username then return end
|
||||
|
||||
if auth_db.set_assigned_privs( username, pack_privileges( privileges ) ) then
|
||||
minetest.notify_authentication_modified( username )
|
||||
minetest.log( "info", "Reset privileges of player '" .. username .. "' in authentication database" )
|
||||
end
|
||||
end,
|
||||
record_login = function ( ) end,
|
||||
reload = function ( ) end,
|
||||
iterate = auth_db.records
|
||||
} )
|
||||
|
||||
auth_db.connect( )
|
||||
auth_filter.is_enabled = true
|
||||
|
||||
__commands( { auth_db = auth_db, auth_filter = auth_filter } )
|
||||
|
||||
print("[auth_rx] mod authentication database loaded" )
|
7
mods/auth_rx/mod.conf
Normal file
7
mods/auth_rx/mod.conf
Normal file
@ -0,0 +1,7 @@
|
||||
name = auth_rx
|
||||
title = Auth Redux
|
||||
author = sorcerykid
|
||||
license = MIT
|
||||
depends = default, formspecs
|
||||
optional_depends =
|
||||
description = Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest. It is designed from the ground up to be robust and secure enough for use on high-traffic Minetest servers, while also addressing a number of outstanding engine bugs.
|
184
mods/auth_rx/samples.mt
Normal file
184
mods/auth_rx/samples.mt
Normal file
@ -0,0 +1,184 @@
|
||||
#####################################################################
|
||||
#
|
||||
# only allow administrator access (by username or IP address)
|
||||
#
|
||||
#####################################################################
|
||||
|
||||
pass any
|
||||
if $addr eq 172.16.100.1
|
||||
if $addr eq 172.16.100.2
|
||||
if $name eq "admin"
|
||||
continue
|
||||
|
||||
fail now
|
||||
|
||||
#####################################################################
|
||||
#
|
||||
# block access from a range of IP addresses
|
||||
#
|
||||
#####################################################################
|
||||
|
||||
try "This subnet is blocked by the administrator."
|
||||
|
||||
fail any
|
||||
if $addr is /192.88.99.6</a
|
||||
if $addr is /203.0.113.?/a
|
||||
if $addr is /192.168.12^14.?/a
|
||||
continue
|
||||
|
||||
pass now
|
||||
|
||||
#####################################################################
|
||||
#
|
||||
# only allow access from whitelisted players
|
||||
#
|
||||
#####################################################################
|
||||
|
||||
try "The account '$name' is not permitted to join this server."
|
||||
|
||||
when $name in @whitelist.txt pass
|
||||
|
||||
fall now
|
||||
|
||||
#####################################################################
|
||||
#
|
||||
# never allow access from blacklisted players
|
||||
#
|
||||
#####################################################################
|
||||
|
||||
try "The account '$name' is not permitted to join this server."
|
||||
|
||||
when $name in @blacklist.txt fail
|
||||
|
||||
pass 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."
|
||||
|
||||
when $name eq uc($name) fail
|
||||
|
||||
pass now
|
||||
|
||||
#####################################################################
|
||||
#
|
||||
# disallow players with very short or very long names
|
||||
#
|
||||
#####################################################################
|
||||
|
||||
try "Sorry, this player name is too long or too short."
|
||||
|
||||
fail any
|
||||
if $name->len() gt 20
|
||||
if $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
|
||||
if $is_new eq $true
|
||||
if $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
|
||||
if $is_new eq $true
|
||||
if ("moderator","server","client","owner","player","system","operator","minetest") has $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
|
||||
if $clock->day() in ("Sat","Sun")
|
||||
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
|
106
mods/auth_rx/tools/convert.awk
Normal file
106
mods/auth_rx/tools/convert.awk
Normal file
@ -0,0 +1,106 @@
|
||||
#!/bin/awk -f
|
||||
|
||||
################################################################################
|
||||
# Database Import Script for Auth Redux Mod
|
||||
# ------------------------------------------
|
||||
# This script will convert the specified 'auth.txt' file into a database format
|
||||
# required by the Auth Redux Mod. The output file will be generated in the same
|
||||
# world directory as the original 'auth.txt' file (which will be unchanged).
|
||||
#
|
||||
# Setting the mode to 'install' will automatically install the required journal
|
||||
# and ruleset files into the world directory as well.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# awk -f convert.awk -v mode=convert ~/.minetest/worlds/world/auth.txt
|
||||
################################################################################
|
||||
|
||||
function error( msg ) {
|
||||
skipped++;
|
||||
print msg " at line " NR " in " FILENAME ".";
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
FS = ":";
|
||||
OFS = ":";
|
||||
checked = 0;
|
||||
skipped = 0;
|
||||
|
||||
db_file = "auth.db";
|
||||
journal_file = "auth.dbx";
|
||||
ruleset_file = "greenlist.mt";
|
||||
|
||||
# determine output file name from arguments
|
||||
|
||||
path = ARGV[ 1 ]
|
||||
if( sub( /[-_A-Za-z0-9]+\.txt$/, "", path ) == 0 ) {
|
||||
# sanity check for nonstandard input file
|
||||
path = "";
|
||||
}
|
||||
|
||||
# install required journal and ruleset files
|
||||
|
||||
if( mode == "install" ) {
|
||||
print "Installing the required journal and ruleset files...";
|
||||
print "" > path journal_file
|
||||
print "pass now" > path ruleset_file
|
||||
}
|
||||
else if( mode != "convert" ) {
|
||||
print "Unknown argument, defaulting to convert mode.";
|
||||
}
|
||||
|
||||
# set default values for new database fields
|
||||
|
||||
approved_addrs = "";
|
||||
oldlogin = -1;
|
||||
lifetime = 0;
|
||||
total_failures = 0;
|
||||
total_attempts = 0;
|
||||
total_sessions = 0;
|
||||
|
||||
# print database headline to the output file
|
||||
|
||||
print "Converting " ARGV[ 1 ] "...";
|
||||
print "auth_rx/2.1 @0" > path db_file;
|
||||
}
|
||||
|
||||
NF != 4 {
|
||||
error( "Malformed record" );
|
||||
next;
|
||||
}
|
||||
|
||||
{
|
||||
username = $1;
|
||||
password = $2;
|
||||
assigned_privs = $3;
|
||||
newlogin = $4;
|
||||
|
||||
if( !match( username, "^[a-zA-Z0-9_-]+$" ) ) {
|
||||
error( "Invalid username field" );
|
||||
next;
|
||||
}
|
||||
if( !match( newlogin, "^[0-9]+$" ) && newlogin != -1 ) {
|
||||
error( "Invalid last_login field" );
|
||||
next;
|
||||
}
|
||||
|
||||
# Database File Format
|
||||
# --------------------
|
||||
# username
|
||||
# password
|
||||
# oldlogin
|
||||
# newlogin
|
||||
# lifetime
|
||||
# total_sessions
|
||||
# total_attempts
|
||||
# total_failures
|
||||
# approved_addrs
|
||||
# assigned_privs
|
||||
|
||||
print username, password, oldlogin, newlogin, lifetime, total_sessions, total_attempts, total_failures, approved_addrs, assigned_privs > path db_file;
|
||||
|
||||
checked++;
|
||||
}
|
||||
|
||||
END {
|
||||
print "Done! " checked " of " ( checked + skipped ) " total records were imported to " db_file " (" skipped " records skipped)."
|
||||
}
|
223
mods/auth_rx/tools/extract.awk
Normal file
223
mods/auth_rx/tools/extract.awk
Normal file
@ -0,0 +1,223 @@
|
||||
#!/bin/awk -f
|
||||
|
||||
################################################################################
|
||||
# Database Import Script for Auth Redux Mod (Step 2)
|
||||
# ---------------------------------------------------
|
||||
# WARNING: This script is to be run immediately after the database conversion.
|
||||
#
|
||||
# This script will extract player login activity from the specified 'debug.txt'
|
||||
# file and produce an import journal in the world directory. For best results,
|
||||
# the 'auth.txt' and 'debug.txt' files should be complete and current. If they
|
||||
# are not entirely synchronous, then errors are likely to result.
|
||||
#
|
||||
# Lastly, run the rollback.lua script from the command line. This will replay
|
||||
# the import journal against the newly converted database, applying all player
|
||||
# login activity as it was derived from the corresponding debug log.
|
||||
#
|
||||
# Below is the required sequence of commands:
|
||||
#
|
||||
# > awk -f convert.awk -v mode=convert ~/.minetest/worlds/world/auth.txt
|
||||
# > awk -f extract.awk ~/.minetest/worlds/world/auth.txt ~/.minetest/debug.txt
|
||||
# > lua rollback.lua ~/.minetest/worlds/world/auth.db
|
||||
#
|
||||
# It is not necessary to operate on the original files. They can be moved into
|
||||
# into a temporary subdirectory for use with all of these scripts.
|
||||
#
|
||||
# WARNING: The 'auth.dbx' file will be overwritten. This should not cause any
|
||||
# problems if you began with a freshly converted database. When in doubt, then
|
||||
# you may want to backup the 'auth.db' and 'auth.dbx' files as a precaution.
|
||||
#
|
||||
# For more detailed output, you can change the debug level as follows:
|
||||
#
|
||||
# > awk -f extract.awk -v debug=verbose ...
|
||||
#
|
||||
# This will display both errors and warnings. It might be helpful to redirect
|
||||
# output to a temporary file for this purpose. To see only errors, the default
|
||||
# debug level of 'terse' should be sufficient.
|
||||
#
|
||||
# Warnings occur if there are orphaned accounts in the 'auth.txt' file that do
|
||||
# not appear in the corresponding debug log. This happens when the debug log
|
||||
# is incomplete. Orphaned accounts will have no player login activity applied.
|
||||
#
|
||||
# Errors occur if the 'auth.txt' file is inconsistent with the debug log. This
|
||||
# could be the result of a server crash or intentional deletion of accounts.
|
||||
# Such accounts are deemed invalid and player login activity will be ignored.
|
||||
# However, you should verify that you are using the correct 'auth.txt' file.
|
||||
################################################################################
|
||||
|
||||
# USAGE EXAMPLE:
|
||||
# awk -f extract.awk -v debug=terse /home/minetest/.minetest/worlds/world/auth.txt /home/minetest/.minetest/debug.txt
|
||||
|
||||
function get_timestamp( date_str, time_str ) {
|
||||
return mktime( sprintf( "%d %d %d %d %d %d", \
|
||||
substr( date_str, 1, 4 ), substr( date_str, 6, 2 ), substr( date_str, 9, 2 ), substr( time_str, 1, 2 ), substr( time_str, 4, 2 ), substr( time_str, 7, 2 ) ) );
|
||||
}
|
||||
|
||||
function print_info( source, result ) {
|
||||
if( is_verbose == 1 ) {
|
||||
print "[" source "]", result;
|
||||
}
|
||||
}
|
||||
|
||||
function check_user( name ) {
|
||||
if( name in db_users ) {
|
||||
++db_users[ name ];
|
||||
return 0;
|
||||
}
|
||||
|
||||
if( !log_users[ name ] ) {
|
||||
print "ERROR: Player '" name "' does not exist in auth.txt file.";
|
||||
}
|
||||
return ++log_users[ name ];
|
||||
}
|
||||
|
||||
function trim( str ) {
|
||||
return substr( str, 2, length( str ) - 2 )
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
FS = " "
|
||||
|
||||
LOG_STARTED = 10
|
||||
LOG_STOPPED = 12
|
||||
LOG_TOUCHED = 13
|
||||
TX_SESSION_OPENED = 50
|
||||
TX_SESSION_CLOSED = 51
|
||||
TX_LOGIN_FAILURE = 31
|
||||
TX_LOGIN_SUCCESS = 32
|
||||
|
||||
journal_file = "auth.dbx";
|
||||
is_started = 0;
|
||||
is_verbose = 0;
|
||||
|
||||
total_records = 0;
|
||||
|
||||
if( debug == "verbose" ) {
|
||||
is_verbose = 1;
|
||||
}
|
||||
else if( debug != "terse" ) {
|
||||
print "ERROR: Unknown argument, defaulting to terse debug level.";
|
||||
}
|
||||
|
||||
if( ARGC != 3 ) {
|
||||
print( "The required arguments are missing, aborting." )
|
||||
exit 1;
|
||||
}
|
||||
|
||||
world_path = ARGV[ 1 ];
|
||||
if( sub( /auth.txt$/, "", world_path ) == 0 ) {
|
||||
print( "The specified auth.txt file is not recognized, aborting." )
|
||||
exit 1;
|
||||
}
|
||||
|
||||
if( ARGV[ 2 ] != "debug.txt" && ARGV[ 2 ] !~ /\/debug.txt$/ ) {
|
||||
print( "The specified debug.txt file is not recognized, aborting." )
|
||||
exit 1;
|
||||
}
|
||||
}
|
||||
|
||||
# parse the 'auth.txt' file
|
||||
|
||||
ARGIND == 1 && FNR == 1 {
|
||||
print( "Reading the " world_path "auth.txt file..." )
|
||||
}
|
||||
|
||||
ARGIND == 1 {
|
||||
name = substr( $0, 1, index( $0, ":" ) - 1 )
|
||||
db_users[ name ] = 0;
|
||||
total_records++;
|
||||
}
|
||||
|
||||
# parse the 'debug.txt' file
|
||||
|
||||
ARGIND == 2 && FNR == 1 {
|
||||
print( "Reading the " world_path "debug.txt file..." )
|
||||
}
|
||||
|
||||
ARGIND == 2 {
|
||||
cur_date_str = $1;
|
||||
cur_time_str = $2;
|
||||
|
||||
if( $3 == "ACTION[Main]:" && ( $4 FS $5 ) == "World at" ) {
|
||||
|
||||
# 2018-07-10 12:16:09: ACTION[Main]: World at [/root/.minetest/worlds/world]
|
||||
print_info( "debug.txt", $1 " @connect" );
|
||||
|
||||
print get_timestamp( cur_date_str, cur_time_str ), LOG_STARTED > ( world_path journal_file );
|
||||
is_started = 1;
|
||||
}
|
||||
else if( !is_started ) {
|
||||
# sanity check since mod loading errors precede startup logging
|
||||
# and shutdowns are not always immediate after they are logged.
|
||||
next;
|
||||
}
|
||||
else if( $3 == "[Main]:" && $5 == "sigint_handler():" || $3 == "ERROR[Main]:" && ( $4 FS $5 ) == "stack traceback:" ) {
|
||||
|
||||
# 2018-07-10 06:47:46: [Main]: INFO: sigint_handler(): Ctrl-C pressed, shutting down.
|
||||
# 2018-07-10 16:18:52: ERROR[Main]: stack traceback:
|
||||
print_info( "debug.txt", $1 " @disconnect " ( $4 == "stack" ? "(err)" : "(sig)" ) );
|
||||
|
||||
print get_timestamp( cur_date_str, cur_time_str ), LOG_STOPPED > ( world_path journal_file );
|
||||
is_started = 0;
|
||||
}
|
||||
else if( $3 == "ACTION[Server]:" && ( $5 == "joins" || $6 == "joins" || $5 == "leaves" || $5 == "times" || $5 == "shuts" || $4 == "Server:" ) ) { # optimization hack
|
||||
cur_action_str = substr( $0, 38 );
|
||||
|
||||
if( cur_action_str ~ /^[a-zA-Z0-9_-]+ \[[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\] joins game\./ ) {
|
||||
|
||||
# 2017-06-09 16:49:26: ACTION[Server]: sorcerykid [127.0.0.1] joins game.
|
||||
# print_info( "debug.txt", $1 " @on_login_success " $4 FS trim( $5 ) );
|
||||
|
||||
if( check_user( $4 ) > 0 ) next;
|
||||
print get_timestamp( cur_date_str, cur_time_str ), TX_LOGIN_SUCCESS, $4, trim( $5 ) > ( world_path journal_file );
|
||||
}
|
||||
else if( cur_action_str ~ /^Server: User [a-zA-Z0-9_-]+ at [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ supplied wrong password/ ) {
|
||||
|
||||
# 2017-06-09 20:35:20: ACTION[Server]: Server: User sorcerykid at 127.0.0.1 supplied wrong password (auth mechanism: SRP)
|
||||
# print_info( "debug.txt", $1 " @on_login_failure " $6 FS $8 );
|
||||
|
||||
if( check_user( $6 ) > 0 ) next;
|
||||
print get_timestamp( cur_date_str, cur_time_str ), TX_LOGIN_FAILURE, $6, $8 > ( world_path journal_file );
|
||||
}
|
||||
else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ joins game\./ ) {
|
||||
|
||||
# 2017-06-09 16:49:26: ACTION[Server]: sorcerykid joins game. List of players:
|
||||
# print_info( "debug.txt", $1 " @on_session_opened " $4 );
|
||||
|
||||
if( check_user( $4 ) > 0 ) next;
|
||||
print get_timestamp( cur_date_str, cur_time_str ), TX_SESSION_OPENED, $4 > ( world_path journal_file );
|
||||
}
|
||||
else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ leaves game\./ || cur_action_str ~ /[a-zA-Z0-9_-]+ times out\./ ) {
|
||||
|
||||
# 2017-06-09 20:32:32: ACTION[Server]: sorcerykid leaves game. List of players:
|
||||
# 2017-06-09 20:34:47: ACTION[Server]: sorcerykid times out. List of players:
|
||||
# print_info( "debug.txt", $1 " @on_session_closed " $4 );
|
||||
|
||||
if( check_user( $4 ) > 0 ) next;
|
||||
print get_timestamp( cur_date_str, cur_time_str ), TX_SESSION_CLOSED, $4 > ( world_path journal_file );
|
||||
}
|
||||
else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ shuts down server/ ) {
|
||||
|
||||
# 2017-06-09 20:32:32: ACTION[Server]: sorcerykid shuts down server
|
||||
# print_info( "debug.txt", $1 " @disconnect (req)" );
|
||||
|
||||
print get_timestamp( cur_date_str, cur_time_str ), LOG_STOPPED > ( world_path journal_file );
|
||||
is_started = 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
total_orphans = 0;
|
||||
for( name in db_users ) {
|
||||
if( db_users[ name ] == 0 ) {
|
||||
if( is_verbose == 1 ) {
|
||||
print "WARNING: No player activity for '" name "' in debug.txt file.";
|
||||
}
|
||||
total_orphans++;
|
||||
}
|
||||
}
|
||||
print "Total accounts in database: " total_records " (" total_orphans " orphaned accounts)";
|
||||
print "Done!"
|
||||
}
|
335
mods/auth_rx/tools/report.awk
Executable file
335
mods/auth_rx/tools/report.awk
Executable file
@ -0,0 +1,335 @@
|
||||
#!/bin/awk -f
|
||||
########################################################
|
||||
# #
|
||||
# Minetest :: Auth Redux Mod v2.11 (auth_rx) #
|
||||
# #
|
||||
# See README.txt for licensing and release notes. #
|
||||
# Copyright (c) 2017-2018, Leslie E. Krause #
|
||||
# #
|
||||
########################################################
|
||||
|
||||
# Run this script and optionally redirect output to a text file
|
||||
# awk -f report.awk -v days=1 type=txt ~/.minetest/worlds/world/auth.dbx
|
||||
|
||||
function throw( msg ) {
|
||||
print msg > "/dev/tty";
|
||||
status = 1;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
function get_period( t ) {
|
||||
return int( ( t - rel_time ) / 3600 )
|
||||
}
|
||||
|
||||
function on_server_startup( cur_time ) {
|
||||
server_start = cur_time
|
||||
}
|
||||
|
||||
function on_server_shutdown( cur_time, _old_time, _new_time ) {
|
||||
_old_time = server_start < rel_time ? rel_time : server_start;
|
||||
_new_time = cur_time >= rel_time + 86400 ? rel_time + 86399 : cur_time;
|
||||
server_uptime = server_uptime + ( _new_time - _old_time );
|
||||
server_start = NIL;
|
||||
}
|
||||
|
||||
function on_login_failure( cur_time ) {
|
||||
hourly_failures[ get_period( cur_time ) ]++;
|
||||
total_failures++;
|
||||
}
|
||||
|
||||
function on_login_attempt( cur_time ) {
|
||||
hourly_attempts[ get_period( cur_time ) ]++;
|
||||
total_attempts++;
|
||||
}
|
||||
function on_create( cur_time, cur_user ) {
|
||||
player_is_new[ cur_user ] = 1;
|
||||
}
|
||||
|
||||
function on_session_opened( cur_time, cur_user ) {
|
||||
while( cur_period < get_period( cur_time ) ) {
|
||||
if( hourly_players[ cur_period ] == NIL ) {
|
||||
# initialize client and player stats in prior periods
|
||||
hourly_clients_max[ cur_period ] = cur_clients;
|
||||
hourly_clients_min[ cur_period ] = cur_clients;
|
||||
hourly_players[ cur_period ] = cur_clients;
|
||||
}
|
||||
cur_period++;
|
||||
}
|
||||
|
||||
if( hourly_players[ cur_period ] == NIL ) {
|
||||
# initialize client and player stats for this period
|
||||
hourly_clients_max[ cur_period ] = cur_clients + 1;
|
||||
hourly_clients_min[ cur_period ] = cur_clients;
|
||||
hourly_players[ cur_period ] = cur_clients;
|
||||
delete player_check;
|
||||
}
|
||||
else if( cur_clients + 1 > hourly_clients_max[ cur_period ] ) {
|
||||
# update client stats for this period, if needed
|
||||
hourly_clients_max[ cur_period ] = cur_clients + 1;
|
||||
}
|
||||
if( player_check[ cur_user ] == NIL ) {
|
||||
# track another unique player
|
||||
player_check[ cur_user ] = 1;
|
||||
hourly_players[ cur_period ]++;
|
||||
}
|
||||
|
||||
# update some general stats
|
||||
if( player_is_new[ cur_user ] == 1 ) {
|
||||
# only count new players after joining game (sanity check)
|
||||
player_is_new[ cur_user ] = -1;
|
||||
total_players_new++;
|
||||
}
|
||||
if( max_clients == NIL || cur_clients + 1 > max_clients ) {
|
||||
max_clients = cur_clients + 1;
|
||||
}
|
||||
if( min_clients == NIL || cur_clients < min_clients ) {
|
||||
min_clients = cur_clients;
|
||||
}
|
||||
}
|
||||
|
||||
function on_session_closed( cur_time, cur_user, _old_time, _new_time ) {
|
||||
_old_time = player_login[ cur_user ] < rel_time ? rel_time : player_login[ cur_user ];
|
||||
_new_time = cur_time >= rel_time + 86400 ? rel_time + 86399 : cur_time;
|
||||
lifetime = _new_time - _old_time;
|
||||
|
||||
while( cur_period < get_period( _new_time ) ) {
|
||||
if( hourly_players[ cur_period ] == NIL ) {
|
||||
# initialize client and player stats in prior periods
|
||||
hourly_clients_max[ cur_period ] = cur_clients;
|
||||
hourly_clients_min[ cur_period ] = cur_clients;
|
||||
hourly_players[ cur_period ] = cur_clients;
|
||||
}
|
||||
cur_period++;
|
||||
}
|
||||
|
||||
if( hourly_players[ cur_period ] == NIL ) {
|
||||
# initialize client and player stats for this period
|
||||
hourly_clients_max[ cur_period ] = cur_clients;
|
||||
hourly_clients_min[ cur_period ] = cur_clients - 1;
|
||||
hourly_players[ cur_period ] = cur_clients;
|
||||
delete player_check;
|
||||
}
|
||||
else if( cur_clients - 1 < hourly_clients_min[ cur_period ] ) {
|
||||
# update client stats for this period, if needed
|
||||
hourly_clients_min[ cur_period ] = cur_clients - 1;
|
||||
}
|
||||
if( player_check[ cur_user ] == NIL ) {
|
||||
# track another unique player
|
||||
player_check[ cur_user ] = 1;
|
||||
}
|
||||
|
||||
for( p = get_period( _old_time ); p <= cur_period; p++ ) {
|
||||
# update session stats in all prior periods
|
||||
hourly_sessions[ p ]++;
|
||||
}
|
||||
|
||||
# update some general stats
|
||||
if( lifetime > max_lifetime ) {
|
||||
max_lifetime = lifetime;
|
||||
}
|
||||
if( cur_time < rel_time + 86400 ) {
|
||||
if( max_clients == NIL || cur_clients > max_clients ) {
|
||||
max_clients = cur_clients;
|
||||
}
|
||||
if( min_clients == NIL || cur_clients - 1 < min_clients ) {
|
||||
min_clients = cur_clients - 1;
|
||||
}
|
||||
}
|
||||
if( !player_sessions[ cur_user ] ) {
|
||||
# if no previous sessions, it's a unique player
|
||||
total_players++;
|
||||
}
|
||||
total_sessions++;
|
||||
total_lifetime += lifetime;
|
||||
player_lifetime[ cur_user ] += lifetime;
|
||||
player_sessions[ cur_user ]++;
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
NIL = ""; # undefined variables are ambiguous (either 0 or "", so we'll create our own nil)
|
||||
|
||||
TX_CREATE = 20;
|
||||
TX_SESSION_OPENED = 50;
|
||||
TX_SESSION_CLOSED = 51;
|
||||
TX_LOGIN_ATTEMPT = 30;
|
||||
TX_LOGIN_FAILURE = 31;
|
||||
LOG_STARTED = 10;
|
||||
LOG_CHECKED = 11;
|
||||
LOG_STOPPED = 12;
|
||||
|
||||
stat_bar[ 0 ] = "-";
|
||||
stat_bar[ 1 ] = "\\";
|
||||
stat_bar[ 2 ] = "|";
|
||||
stat_bar[ 3 ] = "/";
|
||||
stat_idx = 0;
|
||||
|
||||
cur_period = 0;
|
||||
|
||||
if( ARGC != 2 ) {
|
||||
throw( "The required arguments are missing, aborting." );
|
||||
}
|
||||
if( ARGV[ 1 ] != "-" && ARGV[ 1 ] !~ /\.dbx$/ ) {
|
||||
throw( "The specified journal file is not recognized, aborting." );
|
||||
}
|
||||
if( days !~ /^[0-9]+$/ ) {
|
||||
throw( "The required 'days' parameter is invalid, aborting." );
|
||||
}
|
||||
if( type != "txt" && type != "js" ) {
|
||||
throw( "The required 'type' parameter is invalid, aborting." );
|
||||
}
|
||||
|
||||
# calculate the relative date from offset
|
||||
rel_date = ( int( systime( ) / 86400 ) - days )
|
||||
rel_time = rel_date * 86400;
|
||||
|
||||
printf "Working on it..." > "/dev/tty";
|
||||
}
|
||||
|
||||
{
|
||||
# show an animated progress indicator
|
||||
if( stat_idx++ % 50001 == 0 ) printf "%s\b", stat_bar[ stat_idx % 4 ] > "/dev/tty";
|
||||
|
||||
cur_time = $1;
|
||||
if( $2 == TX_LOGIN_ATTEMPT ) {
|
||||
if( cur_time >= rel_time && cur_time < rel_time + 86400 ) {
|
||||
on_login_attempt( cur_time )
|
||||
}
|
||||
}
|
||||
else if( $2 == TX_LOGIN_FAILURE ) {
|
||||
if( cur_time >= rel_time && cur_time < rel_time + 86400 ) {
|
||||
on_login_failure( cur_time )
|
||||
}
|
||||
}
|
||||
else if( $2 == TX_CREATE ) {
|
||||
cur_user = $3;
|
||||
if( cur_time >= rel_time && cur_time < rel_time + 86400 ) {
|
||||
on_login_attempt( cur_time )
|
||||
on_create( cur_time, cur_user )
|
||||
}
|
||||
}
|
||||
else if( $2 == TX_SESSION_OPENED ) {
|
||||
# player joined game
|
||||
cur_user = $3;
|
||||
if( cur_time < rel_time + 86400 ) {
|
||||
player_login[ cur_user ] = cur_time;
|
||||
|
||||
if( cur_time >= rel_time ) {
|
||||
# only track sessions within the specified timeframe
|
||||
on_session_opened( cur_time, cur_user )
|
||||
}
|
||||
}
|
||||
cur_clients++;
|
||||
}
|
||||
else if( $2 == TX_SESSION_CLOSED ) {
|
||||
# player left game
|
||||
cur_user = $3;
|
||||
if( cur_time >= rel_time && cur_user in player_login ) {
|
||||
# only track sessions within the specified timeframe
|
||||
on_session_closed( cur_time, cur_user )
|
||||
}
|
||||
cur_clients--;
|
||||
delete player_login[ cur_user ];
|
||||
}
|
||||
else if( $2 == LOG_STARTED ) {
|
||||
if( cur_time < rel_time + 86400 ) {
|
||||
on_server_startup( cur_time )
|
||||
}
|
||||
|
||||
# sanity check (these should already not exist!)
|
||||
delete player_login;
|
||||
cur_clients = 0;
|
||||
}
|
||||
else if( $2 == LOG_STOPPED || $2 == LOG_CHECKED ) {
|
||||
if( cur_time >= rel_time && server_start != NIL ) {
|
||||
on_server_shutdown( cur_time )
|
||||
}
|
||||
|
||||
# on server shutdown, all players logged off
|
||||
for( cur_user in player_login ) {
|
||||
if( cur_time >= rel_time ) {
|
||||
# only track sessions within the specified timeframe
|
||||
on_session_closed( cur_time, cur_user )
|
||||
}
|
||||
}
|
||||
# purge stale data for next server startup
|
||||
delete player_login;
|
||||
cur_clients = 0;
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
# abort during an abnormal condition
|
||||
if( status ) exit;
|
||||
|
||||
printf "Done!\n" > "/dev/tty";
|
||||
avg_lifetime = total_players > 0 ? total_lifetime / total_sessions : 0;
|
||||
|
||||
if( type == "txt" ) {
|
||||
print "Daily Player Analytics Report (" strftime( "%d-%b-%Y UTC", rel_time, 1 ) ")\n";
|
||||
|
||||
print "Player Activity: 24-Hour Totals";
|
||||
print "===========================================";
|
||||
print sprintf( " %-19s %10s %10s", "Player", "Sessions", "Lifetime", "Failures", "Attempts" );
|
||||
print "-------------------------------------------";
|
||||
for( i in player_sessions ) {
|
||||
print sprintf( " %-19s %10d %5dm %02ds", player_is_new[ i ] ? "* " i : i, player_sessions[ i ], player_lifetime[ i ] / 60, player_lifetime[ i ] % 60 );
|
||||
}
|
||||
print "-------------------------------------------";
|
||||
|
||||
print "\nPlayer Activity: Hourly Totals";
|
||||
print "======================================================";
|
||||
print sprintf( " %-8s %10s %10s %10s %10s", "Period", "Sessions", "Failures", "Attempts", "Players" );
|
||||
print "------------------------------------------------------";
|
||||
for( i = 0; i < 24; i++ ) {
|
||||
print sprintf( " [%02d:00] %10s %10s %10s %10s", i,
|
||||
i in hourly_sessions ? hourly_sessions[ i ] : 0,
|
||||
i in hourly_failures ? hourly_failures[ i ] : 0,
|
||||
i in hourly_attempts ? hourly_attempts[ i ] : 0,
|
||||
i in hourly_players ? hourly_players[ i ] : 0 );
|
||||
}
|
||||
print "------------------------------------------------------";
|
||||
|
||||
print "\nPlayer Activity: Hourly Trends";
|
||||
print "===========================================";
|
||||
print sprintf( " %-9s %15s %15s", "Period", "Min Clients", "Max Clients" );
|
||||
print "-------------------------------------------";
|
||||
for( i = 0; i < 24; i++ ) {
|
||||
print sprintf( " [%02d:00] %15s %15s", i,
|
||||
i in hourly_clients_min ? hourly_clients_min[ i ] : 0,
|
||||
i in hourly_clients_max ? hourly_clients_max[ i ] : 0 );
|
||||
}
|
||||
print "-------------------------------------------";
|
||||
|
||||
print "\nPlayer Activity: 24-Hour Summary"
|
||||
print "===========================================";
|
||||
print sprintf( " %-30s %10d", "Total Players:", total_players );
|
||||
print sprintf( " %-30s %10d", "Total New Players:", total_players_new );
|
||||
print sprintf( " %-30s %10d", "Total Player Sessions:", total_sessions );
|
||||
print sprintf( " %-30s %10d", "Total Login Failures:", total_failures );
|
||||
print sprintf( " %-30s %10d", "Total Login Attempts:", total_attempts );
|
||||
print sprintf( " %-30s %9d%%", "Overall Server Uptime:", server_uptime / 86399 * 100 );
|
||||
print sprintf( " %-30s %10d", "Maximum Connected Clients:", max_clients );
|
||||
print sprintf( " %-30s %10d", "Minimum Connected Clients:", min_clients );
|
||||
print sprintf( " %-30s %5dm %02ds", "Maximum Player Lifetime:", max_lifetime / 60, max_lifetime % 60 );
|
||||
print sprintf( " %-30s %5dm %02ds", "Average Player Lifetime:", avg_lifetime / 60, avg_lifetime % 60 );
|
||||
print "-------------------------------------------";
|
||||
}
|
||||
else if( type == "js" ) {
|
||||
printf "{ datespec: %d, filespec: \"%s\", ", rel_date, ARGV[ 1 ];
|
||||
printf "global_stats: { total_players: %d, total_players_new: %d, total_sessions: %d, total_failures: %d, total_attempts: %d, server_uptime: %d, max_clients: %d, min_clients: %d, max_lifetime: %d, avg_lifetime: %d }, ",
|
||||
total_players, total_players_new, total_sessions, total_failures, total_attempts, server_uptime, max_clients, min_clients, max_lifetime, avg_lifetime;
|
||||
printf "hourly_stats: [ ";
|
||||
for( i = 0; i < 24; i++ ) {
|
||||
# printf coerces any nil values to zero automatically
|
||||
printf "{ sessions: %d, failures: %d, attempts: %d, players: %d, clients_max: %d, clients_min: %d }, ",
|
||||
hourly_sessions[ i ], hourly_failures[ i ], hourly_attempts[ i ], hourly_players[ i ], hourly_clients_max[ i ], hourly_clients_min[ i ];
|
||||
}
|
||||
printf "], ";
|
||||
printf "player_stats: { ";
|
||||
for( i in player_sessions ) {
|
||||
printf "\"%s\": { sessions: %d, lifetime: %d }, ", i, player_sessions[ i ], player_lifetime[ i ]
|
||||
}
|
||||
printf "} ";
|
||||
printf "};\n";
|
||||
}
|
||||
}
|
38
mods/auth_rx/tools/revert.awk
Normal file
38
mods/auth_rx/tools/revert.awk
Normal file
@ -0,0 +1,38 @@
|
||||
#!/bin/awk -f
|
||||
|
||||
################################################################################
|
||||
# Database Export Script for Auth Redux Mod
|
||||
# ------------------------------------------
|
||||
# This script will revert to the default 'auth.txt' flat-file database required
|
||||
# by the builtin authentication handler.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# awk -f revert.awk ~/.minetest/worlds/world/auth.db
|
||||
################################################################################
|
||||
|
||||
BEGIN {
|
||||
FS = ":";
|
||||
OFS = ":";
|
||||
db_file = "auth.txt";
|
||||
|
||||
path = ARGV[ 1 ]
|
||||
if( sub( /[-_A-Za-z0-9]+\.db$/, "", path ) == 0 ) {
|
||||
# sanity check for nonstandard input file
|
||||
path = "";
|
||||
}
|
||||
|
||||
print "Reverting " ARGV[ 1 ] "...";
|
||||
}
|
||||
|
||||
NF == 10 {
|
||||
username = $1;
|
||||
password = $2;
|
||||
assigned_privs = $10;
|
||||
newlogin = $4;
|
||||
|
||||
print username, password, assigned_privs, newlogin > path db_file;
|
||||
}
|
||||
|
||||
END {
|
||||
print "Done!"
|
||||
}
|
66
mods/auth_rx/tools/rollback.lua
Normal file
66
mods/auth_rx/tools/rollback.lua
Normal file
@ -0,0 +1,66 @@
|
||||
--------------------------------------------------------
|
||||
-- Minetest :: Auth Redux Mod v2.4 (auth_rx)
|
||||
--
|
||||
-- See README.txt for licensing and release notes.
|
||||
-- Copyright (c) 2017-2018, Leslie E. Krause
|
||||
--------------------------------------------------------
|
||||
|
||||
minetest = { }
|
||||
|
||||
function string.split( str, sep, has_nil )
|
||||
res = { }
|
||||
for val in string.gmatch( str .. sep, "(.-)" .. sep ) do
|
||||
if val ~= "" or has_nil then
|
||||
table.insert( res, val )
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
minetest.log = function ( act, str )
|
||||
print( "[" .. act .. "]", str )
|
||||
end
|
||||
|
||||
minetest.register_globalstep = function ( ) end
|
||||
|
||||
--------------------------------------------------------
|
||||
|
||||
dofile( "../db.lua" )
|
||||
|
||||
local name = "auth.db"
|
||||
local path = "."
|
||||
|
||||
print( "******************************************************" )
|
||||
print( "* This script will rollback the Auth Redux database. *" )
|
||||
print( "* Do not proceed unless you know what you are doing! *" )
|
||||
print( "* -------------------------------------------------- *" )
|
||||
print( "* Usage Example: *" )
|
||||
print( "* lua rollback.lua ~/.minetest/worlds/world/auth.db *" )
|
||||
print( "******************************************************" )
|
||||
|
||||
if arg[ 1 ] and arg[ 1 ] ~= "auth.db" then
|
||||
path = string.match( arg[ 1 ], "^(.*)/auth%.db$" )
|
||||
if not path then
|
||||
error( "Invalid arguments specified." )
|
||||
end
|
||||
end
|
||||
|
||||
print( "The following database will be modified:" )
|
||||
print( " " .. path .. "/" .. name )
|
||||
print( )
|
||||
|
||||
io.write( "Do you wish to continue (y/n)? " )
|
||||
local opt = io.read( 1 )
|
||||
|
||||
if opt == "y" then
|
||||
print( "Initiating rollback procedure..." )
|
||||
|
||||
local auth_db = AuthDatabase( path, name )
|
||||
auth_db.rollback( )
|
||||
|
||||
os.rename( path .. "/" .. name .. "x", path .. "/~" .. name .. "x" )
|
||||
|
||||
if not io.open( path .. "/" .. name .. "x", "w+b" ) then
|
||||
minetest.log( "error", "Cannot open " .. path .. "/~" .. name .. " for writing." )
|
||||
end
|
||||
end
|
47
mods/auth_rx/watchdog.lua
Normal file
47
mods/auth_rx/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
|
447
mods/formspecs/README.md
Normal file
447
mods/formspecs/README.md
Normal file
@ -0,0 +1,447 @@
|
||||
minetest ActiveFormspecs
|
||||
========================
|
||||
|
||||
**active formspecs** self-contained API that provides formspecs secure session
|
||||
tracking, formspecs session-based state tables, and localized event handling of
|
||||
formspecs
|
||||
|
||||
Information
|
||||
------------
|
||||
|
||||
This mod is named `formspecs` an improved version of dinamic secured forms
|
||||
|
||||
![](screenshot.png?raw=true)
|
||||
|
||||
ActiveFormspecs (named `formspecs` is a self-contained API that provides secure session tracking,
|
||||
session-based state tables, and localized event handling of formspecs for individual mods as well
|
||||
as entire games. It evolved out of a recurring need for secure "node formspecs" on my
|
||||
server, while avoiding the burden of "reinventing the wheel" with every new project.
|
||||
|
||||
## Tecnical info
|
||||
|
||||
Since I've had many requests for source code from the Just Test Tribute subgame,
|
||||
I finally decided to release ActiveFormspecs with instructions and code examples
|
||||
so that other mod authors can start making use of this framework as well.
|
||||
It's easy to install and use, and arguably a more robust alternative to the
|
||||
builtin formspecs API.
|
||||
|
||||
ActiveFormspecs is intended to be compatible with all versions of Minetest as possible,
|
||||
currently tested with 0.4.16 and 5.2.
|
||||
|
||||
It has been in continuous use on my server since December 2016 with only minor revisions.
|
||||
So it should prove secure and stable enough for any production environment, as long as
|
||||
you follow the instructions below.
|
||||
|
||||
#### Dependences
|
||||
|
||||
none, core minetest api
|
||||
|
||||
#### Overview:
|
||||
|
||||
ActiveFormspecs is a framework that abstracts the builtin formspec API of Minetest.
|
||||
It is intended to address a number of known security issues related to formspecs:
|
||||
|
||||
* **Secure Session Tracking**
|
||||
Formspec names have been deprecated as they can be easily forged. Now each
|
||||
formspec session is assigned a unique session ID. Due to the persistent nature
|
||||
of the Minetest client-server protocol (unlike HTTP, for example), all session
|
||||
tracking is performed server-side. Negotiation and validation with the client
|
||||
is entirely unnecessary. Thus, integrity of the session ID is always guaranteed.
|
||||
|
||||
* **Session-Based State Table**
|
||||
Since the session ID token is retained throughout the lifetime of the formspec, it is therefore possible
|
||||
to update a formspec dynamically (e.g. in response to an event) with contextual data spanning multiple
|
||||
instances. This data is stored server-side via a session-based state table and it can even be initialized
|
||||
from within the formspec string itself using a new "hidden" element.
|
||||
|
||||
* **Localized Event Handling**
|
||||
The minetest.register_on_player_receive_fields( ) method has also been deprecated. Instead, each formspec
|
||||
is assigned its own callback function at runtime, which allows for completely localized event handling.
|
||||
This callback function is invoked after any event associated with the formspec
|
||||
(hence the moniker "ActiveFormspecs"). Both the meta table and form fields are passed as arguments.
|
||||
|
||||
#### Progress and status
|
||||
|
||||
https://forum.minetest.net/viewtopic.php?f=9&t=19303
|
||||
|
||||
The project is a WIP and will be undergoing continuous development based upon your suggestions as well
|
||||
as my personal needs. Version 3.0 is already underway, and I am planning to introduce substantial
|
||||
improvements to the core functionality. New features and bug-fixes will be announced here as they become
|
||||
available. During significant milestones, I will include a roadmap so as to gauge your feedback about
|
||||
long-term goals. I will make every effort to ensure backward compatibility, when possible.
|
||||
|
||||
#### Usage Instructions:
|
||||
|
||||
An interactive form session monitor can be accessed in-game via the /fs chat command (requires "server" privilege).
|
||||
A realtime summary of form sessions is displayed along with navigation buttons and related statistics.
|
||||
|
||||
![](screenshot.png?raw=true)
|
||||
|
||||
The summary is sorted chronologically and divided into four columns per player:
|
||||
* player - the name of the player viewing the formspec
|
||||
* origin - the mod (or node, if attached) that created the formspec
|
||||
* idletime - the elapsed time since a form-related event or signal
|
||||
* lifetime - the elapsed time since the form was first opened
|
||||
|
||||
A detached formspec can be opened using the minetest.create_form( ) method which returns a hashed session
|
||||
token (guaranteed to be unique):
|
||||
|
||||
* minetest.create_form( state, player_name, formspec, on_close, signal )
|
||||
* state - an optional session-based state-table (can be nil)
|
||||
* player_name - a valid player name
|
||||
* formspec - a standard formspec string
|
||||
* on_close - an optional callback function to be invoked after an event or signal
|
||||
* signal - an optional signal to pass to the on_close( ) callback function
|
||||
|
||||
The form can subsequently be re-opened or closed using the following methods:
|
||||
|
||||
* minetest.update_form( player_name, formspec )
|
||||
* player_name - a valid player name
|
||||
* formspec - a standard formspec string
|
||||
* minetest.destroy_form( player_name )
|
||||
* player_name - a valid player name
|
||||
|
||||
The associated on_close( ) callback function will be automatically invoked with three arguments during a
|
||||
form-related event or signal, including closure. The return value is currently ignored, but in the future
|
||||
it may allow for additional behavior.
|
||||
|
||||
* on_close( state, player, fields )
|
||||
* state - the session-based state table
|
||||
* player - the player object
|
||||
* fields - the table of submitted fields
|
||||
|
||||
In the event of abnormal form session termination, the callback function will receive a signal indicating
|
||||
the specific condition that led to the closure. One of the following values will be stored in the "quit"
|
||||
field:
|
||||
* minetest.FORMSPEC_SIGEXIT (player clicked exit button or pressed esc key)
|
||||
* minetest.FORMSPEC_SIGQUIT (player logged off)
|
||||
* minetest.FORMSPEC_SIGKILL (player was killed)
|
||||
* minetest.FORMSPEC_SIGTERM (server is shutting down)
|
||||
* minetest.FORMSPEC_SIGPROC (programmatic closure)
|
||||
* minetest.FORMSPEC_SIGTIME (timeout reached)
|
||||
* minetest.FORMSPEC_SIGHOLD (child form opened)
|
||||
* minetest.FORMSPEC_SIGCONT (child form closed)
|
||||
|
||||
A non-trappable SIGSTOP signal can also be passed to minetest.destroy_form( ) or minetest.create_form( )
|
||||
to forcibly kill the current form session, thereby avoiding recursion in callbacks during a procedural
|
||||
form closure.
|
||||
|
||||
```
|
||||
local function open_alertbox( player_name, message )
|
||||
local function get_formspec( )
|
||||
local formspec = <generate a basic formspec string here>
|
||||
|
||||
return formspec
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), nil, minetest.FORMSPEC_SIGSTOP )
|
||||
end
|
||||
```
|
||||
|
||||
When form closure may produce unanticipated side-effects, such as the example above, then the SIGSTOP
|
||||
signal can prove essential to avoid or even defer such actions.
|
||||
|
||||
Cascading forms such as dialog boxes, alerts, etc. can be implemented through the use of the SIGHOLD
|
||||
and SIGCONT signals. By passing a SIGHOLD signal to minetest.create_form(), the callback will be
|
||||
notified of the signal and the form session will be suspended (this also stops any form timers).
|
||||
As soon as the child form is closed, the previous form session will be restored and the callback
|
||||
will be notified of a SIGCONT signal. ActiveFormspecs manages all of the session housekeeping behind
|
||||
the scenes.
|
||||
|
||||
Here is an example of a popup alert that, when opened, will temporarily suspend the current form
|
||||
session until closed:
|
||||
|
||||
```
|
||||
local function open_alert( player_name, message )
|
||||
local get_formspec = function ( )
|
||||
local formspec = "size[4,3]"
|
||||
.. default.gui_bg_img
|
||||
.. string.format( "label[0.5,0.5;%s]", minetest.formspec_escape( message ) )
|
||||
.. "button_exit[1.0,2;2.0,1;close;Close]"
|
||||
return formspec
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), nil, minetest.FORMSPEC_SIGHOLD )
|
||||
end
|
||||
```
|
||||
|
||||
One caveat is that the parent form must be updated whenever a SIGCONT signal is received. This is for
|
||||
security reasons, given that element states (like dropdowns, scrollbars, etc.) cannot be preserved
|
||||
automatically, and some elements may even contain stale data (as with the case of textareas, fields,
|
||||
etc.) which should not be preserved anyway. Thankfully, only one line of code is required:
|
||||
|
||||
```
|
||||
if fields.quit == minetest.FORMSPEC_SIGCONT then minetest.update_form( player_name, get_formspec( ) ) end
|
||||
```
|
||||
|
||||
Formspecs are also supported within a node definition. If an on_open( ) method is defined, then it will
|
||||
be invoked whenever the player right-clicks the node. The return value must be a standard formspec string,
|
||||
or nil to abort.
|
||||
|
||||
* nodedef.on_open( pos, player, fields )
|
||||
* pos - the position of the node (or your own state table)
|
||||
* player - the player object
|
||||
|
||||
The on_close( ) method is invoked during any formspec event or signal. If defined,
|
||||
it will receive three arguments. Currently, the return value is ignored, but in the
|
||||
future it may allow for additional behavior.
|
||||
|
||||
* nodedef.on_close( pos, player, fields )
|
||||
* pos - the position of the node (or your own state table)
|
||||
* player - the player object
|
||||
* fields - the table of submitted fields
|
||||
|
||||
An optional before_open( ) method may also be defined. It should return a custom
|
||||
state table to be subsequently passed to the on_open( ) method in place of the
|
||||
default "pos" parameter.
|
||||
|
||||
* nodedef.before_open( pos, node, player )
|
||||
* pos - the position of the node (or your own state table)
|
||||
* node - the node
|
||||
* player - the player object
|
||||
|
||||
The "hidden" formspec element of allows for a slightly different workflow, by
|
||||
presetting the state table from the formspec string itself. The key-value pairs
|
||||
are used only for initialization and will never be transmitted to the client.
|
||||
An optional data type can be specified within the element as follows:
|
||||
* hidden[name;value;string]
|
||||
* hidden[name;value;boolean]
|
||||
* hidden[name;value;number]
|
||||
* hidden[name;value]
|
||||
|
||||
If the data type is not specified, then no type-conversion will occur. Here are
|
||||
some examples:
|
||||
|
||||
* hidden[pos_x;-240;number]"
|
||||
* hidden[wool_color;darkgreen;string]"
|
||||
* hidden[is_dead;true;boolean]"
|
||||
|
||||
Through the use of form timers, it possible to re-open a formspec at regular intervals,
|
||||
or even to close the formspec after a specified period. There is no need for complex
|
||||
chains of minetest.after( ) or additional globalstep registrations. A form timer persists
|
||||
for the duration of a form session.
|
||||
|
||||
* minetest.get_form_timer( player_name, form_name )
|
||||
|
||||
Returns a form timer object associated with the form session of a given player.
|
||||
Three methods are exposed by the form timer object, providing similar behavior to node timers.
|
||||
* timer.start( timeout ) Starts a timer with the given timeout in seconds.
|
||||
* timer.stop( ) Cancels a running timer.
|
||||
* timer.get_state( ) Returns a table with information about the running timer
|
||||
|
||||
The table returned including:
|
||||
* elapsed - the number of seconds since the timer started
|
||||
* remain - the number of seconds until the timer expires
|
||||
* overrun - the number of milliseconds overrun for this period (only valid within an on_close( ) callback)
|
||||
* counter - the number of periods that have accrued
|
||||
|
||||
The associated on_close( ) callback function will be notified of timer expiration via a SIG_TIME signal.
|
||||
|
||||
#### Code examples
|
||||
|
||||
To better understand this methodology, here are some working code examples. Let's say that we want to register an "uptime" chat command that displays the current server uptime to any player, with an option to automatically refresh the formspec each second.
|
||||
|
||||
```
|
||||
minetest.register_chatcommand( "uptime", {
|
||||
description = "View the uptime of the server interactively",
|
||||
func = function( player_name, param )
|
||||
local is_refresh = true
|
||||
|
||||
local get_formspec = function( )
|
||||
local uptime = minetest.get_server_uptime( )
|
||||
|
||||
local formspec = "size[4,2]"
|
||||
.. string.format( "label[0.5,0.5;%s %d secs]",
|
||||
minetest.colorize( "#FFFF00", "Server Uptime:" ), uptime
|
||||
)
|
||||
.. "checkbox[0.5,1;is_refresh;Auto Refresh;" .. tostring( is_refresh ) .. "]"
|
||||
return formspec
|
||||
end
|
||||
local on_close = function( state, player, fields )
|
||||
if fields.quit == minetest.FORMSPEC_SIGTIME then
|
||||
minetest.update_form( player_name, get_formspec( ) )
|
||||
|
||||
elseif fields.is_refresh then
|
||||
is_refresh = fields.is_refresh == "true"
|
||||
if is_refresh == true then
|
||||
minetest.get_form_timer( player_name ).start( 1 )
|
||||
else
|
||||
minetest.get_form_timer( player_name ).stop( )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), on_close )
|
||||
minetest.get_form_timer( player_name ).start( 1 )
|
||||
end
|
||||
} )
|
||||
```
|
||||
|
||||
Of course, we could implement similar functionality without the need for a chat command. Perhaps we want to display the server uptime only when a privileged player clicks on a Nyan Cat while limiting the total number of refreshes to ten.
|
||||
|
||||
```
|
||||
minetest.register_privilege( "uptime", "View the uptime of the server interactively" )
|
||||
|
||||
local function open_system_monitor( player_name, is_minutes )
|
||||
local view_count = 0
|
||||
local view_limit = 10
|
||||
|
||||
local function get_formspec( )
|
||||
local uptime = minetest.get_server_uptime( )
|
||||
local formspec = "size[4,3]"
|
||||
.. string.format( "label[0.5,0.5;%s %0.1f %s]",
|
||||
minetest.colorize( "#FFFF00", "Server Uptime:" ),
|
||||
is_minutes and uptime / 60 or uptime,
|
||||
is_minutes and "mins" or "secs"
|
||||
)
|
||||
.. "checkbox[0.5,1;is_minutes;Show Minutes;" .. tostring( is_minutes ) .. "]"
|
||||
.. "button[0.5,2;2.5,1;update;Refresh]"
|
||||
.. "hidden[view_count;1;number]"
|
||||
.. "hidden[view_limit;10;number]"
|
||||
return formspec
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), function( state, player, fields )
|
||||
if not minetest.check_player_privs( player_name, "uptime" ) then -- sanity check
|
||||
return
|
||||
end
|
||||
|
||||
if fields.update then
|
||||
-- limit the number of refreshes!
|
||||
if view_count == view_limit then
|
||||
minetest.destroy_form( player_name )
|
||||
minetest.chat_send_player( player_name, "You've exceeded the refresh limit." )
|
||||
else
|
||||
view_count = view_count + 1
|
||||
minetest.update_form( player_name, get_formspec( ) )
|
||||
end
|
||||
|
||||
elseif fields.is_minutes then
|
||||
is_minutes = fields.is_minutes == "true"
|
||||
minetest.update_form( player_name, get_formspec( ) )
|
||||
end
|
||||
end )
|
||||
end
|
||||
|
||||
minetest.override_item( "nyancat:nyancat", {
|
||||
description = "System Monitor",
|
||||
|
||||
on_rightclick = function( pos, node, player )
|
||||
local player_name = player:get_player_name( )
|
||||
|
||||
if minetest.check_player_privs( player_name, "uptime" ) then
|
||||
open_system_monitor( player_name, true )
|
||||
else
|
||||
minetest.chat_send_player( player_name, "Your privileges are insufficient." )
|
||||
end
|
||||
end,
|
||||
} )
|
||||
```
|
||||
|
||||
Revision History
|
||||
----------------------
|
||||
|
||||
Version 1.0a (15-Dec-2016)
|
||||
- initial version within default mod
|
||||
|
||||
Version 1.1b (16-Dec-2016)
|
||||
- added better comments
|
||||
|
||||
Version 1.2b (18-Dec-2016)
|
||||
- renamed public methods
|
||||
|
||||
Version 1.3b (04-Jan-2017)
|
||||
- fixed logic of quit event to require valid session
|
||||
|
||||
Version 1.4b (26-Jul-2017)
|
||||
- added method to update formspec and maintain session
|
||||
|
||||
Version 2.0 (24-Dec-2017)
|
||||
- separated all routines into new mod for public release
|
||||
|
||||
Version 2.1 (08-Jan-2018)
|
||||
- various code refactoring and better comments
|
||||
- introduced password hashing of form names
|
||||
- improved sanity checks during form submission
|
||||
- fully reworked parsing of hidden elements
|
||||
- ensured hidden elements are always stripped
|
||||
- gave hidden elements default-state behavior
|
||||
- localized old node registration functions
|
||||
- included player object within form table
|
||||
- added signal handling on formspec termination
|
||||
- added support for callbacks in node overrides
|
||||
|
||||
Version 2.2 (19-Jan-2018)
|
||||
- introduced player-name arguments for all API calls
|
||||
- added signal for programmatic formspec closure
|
||||
- ensured callbacks are notified of session resets
|
||||
- renamed some local variables to improve clarity
|
||||
|
||||
Version 2.3 (28-Jan-2018)
|
||||
- corrected erroneous value of formspec exit signal
|
||||
- removed two experimental form session methods
|
||||
- included timestamp and origin within form table
|
||||
- added form session validation on destroy and update
|
||||
- introduced form timers with start and stop methods
|
||||
- created routine to notify callbacks of timeout
|
||||
- added support for internal statistical tracking
|
||||
- added chat command to view form session summary
|
||||
|
||||
Version 2.4 (12-Feb-2018)
|
||||
- various code refactoring and better comments
|
||||
- full rewrite of timer queue for higher precision
|
||||
|
||||
Version 2.5 (01-Feb-2019)
|
||||
- made callback function optional with default no-op
|
||||
- added non-trappable form session termination signal
|
||||
- properly reset timestamp for lifetime calculation
|
||||
|
||||
Version 2.6 (02-Feb-2020)
|
||||
- added callback to preset state of node formspecs
|
||||
- removed experimental property from node definition
|
||||
- implemented reverse-lookups for dropdown fields
|
||||
- extended dropdown element with optional parameter
|
||||
- added signal to suspend and restore form sessions
|
||||
- combined element parsers into dedicated function
|
||||
- added functions for escaping formspec strings
|
||||
- revamped element parsers to ignore malformed tags
|
||||
- added conditional pattern matching helper function
|
||||
- compatability bumped to Minetest 0.4.15+
|
||||
|
||||
Compatibility
|
||||
----------------------
|
||||
|
||||
Minetest 0.4.15+ required or 5.X
|
||||
|
||||
Installation
|
||||
----------------------
|
||||
|
||||
1. Unzip the archive into the mods directory or game mods directory
|
||||
2. Get sure to the name of this directory to "formspecs" (if you cloned or decompress)
|
||||
3. Add "formspecs" as a dependency to any mods using the API.
|
||||
|
||||
Source Code License
|
||||
----------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2020, Leslie E. Krause (leslie@searstower.org) aka sorceredkid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more details:
|
||||
https://opensource.org/licenses/MIT
|
447
mods/formspecs/README.txt
Normal file
447
mods/formspecs/README.txt
Normal file
@ -0,0 +1,447 @@
|
||||
minetest ActiveFormspecs
|
||||
========================
|
||||
|
||||
**active formspecs** self-contained API that provides formspecs secure session
|
||||
tracking, formspecs session-based state tables, and localized event handling of
|
||||
formspecs
|
||||
|
||||
Information
|
||||
------------
|
||||
|
||||
This mod is named `formspecs` an improved version of dinamic secured forms
|
||||
|
||||
![](screenshot.png?raw=true)
|
||||
|
||||
ActiveFormspecs (named `formspecs` is a self-contained API that provides secure session tracking,
|
||||
session-based state tables, and localized event handling of formspecs for individual mods as well
|
||||
as entire games. It evolved out of a recurring need for secure "node formspecs" on my
|
||||
server, while avoiding the burden of "reinventing the wheel" with every new project.
|
||||
|
||||
## Tecnical info
|
||||
|
||||
Since I've had many requests for source code from the Just Test Tribute subgame,
|
||||
I finally decided to release ActiveFormspecs with instructions and code examples
|
||||
so that other mod authors can start making use of this framework as well.
|
||||
It's easy to install and use, and arguably a more robust alternative to the
|
||||
builtin formspecs API.
|
||||
|
||||
ActiveFormspecs is intended to be compatible with all versions of Minetest as possible,
|
||||
currently tested with 0.4.16 and 5.2.
|
||||
|
||||
It has been in continuous use on my server since December 2016 with only minor revisions.
|
||||
So it should prove secure and stable enough for any production environment, as long as
|
||||
you follow the instructions below.
|
||||
|
||||
#### Dependences
|
||||
|
||||
none, core minetest api
|
||||
|
||||
#### Overview:
|
||||
|
||||
ActiveFormspecs is a framework that abstracts the builtin formspec API of Minetest.
|
||||
It is intended to address a number of known security issues related to formspecs:
|
||||
|
||||
* **Secure Session Tracking**
|
||||
Formspec names have been deprecated as they can be easily forged. Now each
|
||||
formspec session is assigned a unique session ID. Due to the persistent nature
|
||||
of the Minetest client-server protocol (unlike HTTP, for example), all session
|
||||
tracking is performed server-side. Negotiation and validation with the client
|
||||
is entirely unnecessary. Thus, integrity of the session ID is always guaranteed.
|
||||
|
||||
* **Session-Based State Table**
|
||||
Since the session ID token is retained throughout the lifetime of the formspec, it is therefore possible
|
||||
to update a formspec dynamically (e.g. in response to an event) with contextual data spanning multiple
|
||||
instances. This data is stored server-side via a session-based state table and it can even be initialized
|
||||
from within the formspec string itself using a new "hidden" element.
|
||||
|
||||
* **Localized Event Handling**
|
||||
The minetest.register_on_player_receive_fields( ) method has also been deprecated. Instead, each formspec
|
||||
is assigned its own callback function at runtime, which allows for completely localized event handling.
|
||||
This callback function is invoked after any event associated with the formspec
|
||||
(hence the moniker "ActiveFormspecs"). Both the meta table and form fields are passed as arguments.
|
||||
|
||||
#### Progress and status
|
||||
|
||||
https://forum.minetest.net/viewtopic.php?f=9&t=19303
|
||||
|
||||
The project is a WIP and will be undergoing continuous development based upon your suggestions as well
|
||||
as my personal needs. Version 3.0 is already underway, and I am planning to introduce substantial
|
||||
improvements to the core functionality. New features and bug-fixes will be announced here as they become
|
||||
available. During significant milestones, I will include a roadmap so as to gauge your feedback about
|
||||
long-term goals. I will make every effort to ensure backward compatibility, when possible.
|
||||
|
||||
#### Usage Instructions:
|
||||
|
||||
An interactive form session monitor can be accessed in-game via the /fs chat command (requires "server" privilege).
|
||||
A realtime summary of form sessions is displayed along with navigation buttons and related statistics.
|
||||
|
||||
![](screenshot.png?raw=true)
|
||||
|
||||
The summary is sorted chronologically and divided into four columns per player:
|
||||
* player - the name of the player viewing the formspec
|
||||
* origin - the mod (or node, if attached) that created the formspec
|
||||
* idletime - the elapsed time since a form-related event or signal
|
||||
* lifetime - the elapsed time since the form was first opened
|
||||
|
||||
A detached formspec can be opened using the minetest.create_form( ) method which returns a hashed session
|
||||
token (guaranteed to be unique):
|
||||
|
||||
* minetest.create_form( state, player_name, formspec, on_close, signal )
|
||||
* state - an optional session-based state-table (can be nil)
|
||||
* player_name - a valid player name
|
||||
* formspec - a standard formspec string
|
||||
* on_close - an optional callback function to be invoked after an event or signal
|
||||
* signal - an optional signal to pass to the on_close( ) callback function
|
||||
|
||||
The form can subsequently be re-opened or closed using the following methods:
|
||||
|
||||
* minetest.update_form( player_name, formspec )
|
||||
* player_name - a valid player name
|
||||
* formspec - a standard formspec string
|
||||
* minetest.destroy_form( player_name )
|
||||
* player_name - a valid player name
|
||||
|
||||
The associated on_close( ) callback function will be automatically invoked with three arguments during a
|
||||
form-related event or signal, including closure. The return value is currently ignored, but in the future
|
||||
it may allow for additional behavior.
|
||||
|
||||
* on_close( state, player, fields )
|
||||
* state - the session-based state table
|
||||
* player - the player object
|
||||
* fields - the table of submitted fields
|
||||
|
||||
In the event of abnormal form session termination, the callback function will receive a signal indicating
|
||||
the specific condition that led to the closure. One of the following values will be stored in the "quit"
|
||||
field:
|
||||
* minetest.FORMSPEC_SIGEXIT (player clicked exit button or pressed esc key)
|
||||
* minetest.FORMSPEC_SIGQUIT (player logged off)
|
||||
* minetest.FORMSPEC_SIGKILL (player was killed)
|
||||
* minetest.FORMSPEC_SIGTERM (server is shutting down)
|
||||
* minetest.FORMSPEC_SIGPROC (programmatic closure)
|
||||
* minetest.FORMSPEC_SIGTIME (timeout reached)
|
||||
* minetest.FORMSPEC_SIGHOLD (child form opened)
|
||||
* minetest.FORMSPEC_SIGCONT (child form closed)
|
||||
|
||||
A non-trappable SIGSTOP signal can also be passed to minetest.destroy_form( ) or minetest.create_form( )
|
||||
to forcibly kill the current form session, thereby avoiding recursion in callbacks during a procedural
|
||||
form closure.
|
||||
|
||||
```
|
||||
local function open_alertbox( player_name, message )
|
||||
local function get_formspec( )
|
||||
local formspec = <generate a basic formspec string here>
|
||||
|
||||
return formspec
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), nil, minetest.FORMSPEC_SIGSTOP )
|
||||
end
|
||||
```
|
||||
|
||||
When form closure may produce unanticipated side-effects, such as the example above, then the SIGSTOP
|
||||
signal can prove essential to avoid or even defer such actions.
|
||||
|
||||
Cascading forms such as dialog boxes, alerts, etc. can be implemented through the use of the SIGHOLD
|
||||
and SIGCONT signals. By passing a SIGHOLD signal to minetest.create_form(), the callback will be
|
||||
notified of the signal and the form session will be suspended (this also stops any form timers).
|
||||
As soon as the child form is closed, the previous form session will be restored and the callback
|
||||
will be notified of a SIGCONT signal. ActiveFormspecs manages all of the session housekeeping behind
|
||||
the scenes.
|
||||
|
||||
Here is an example of a popup alert that, when opened, will temporarily suspend the current form
|
||||
session until closed:
|
||||
|
||||
```
|
||||
local function open_alert( player_name, message )
|
||||
local get_formspec = function ( )
|
||||
local formspec = "size[4,3]"
|
||||
.. default.gui_bg_img
|
||||
.. string.format( "label[0.5,0.5;%s]", minetest.formspec_escape( message ) )
|
||||
.. "button_exit[1.0,2;2.0,1;close;Close]"
|
||||
return formspec
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), nil, minetest.FORMSPEC_SIGHOLD )
|
||||
end
|
||||
```
|
||||
|
||||
One caveat is that the parent form must be updated whenever a SIGCONT signal is received. This is for
|
||||
security reasons, given that element states (like dropdowns, scrollbars, etc.) cannot be preserved
|
||||
automatically, and some elements may even contain stale data (as with the case of textareas, fields,
|
||||
etc.) which should not be preserved anyway. Thankfully, only one line of code is required:
|
||||
|
||||
```
|
||||
if fields.quit == minetest.FORMSPEC_SIGCONT then minetest.update_form( player_name, get_formspec( ) ) end
|
||||
```
|
||||
|
||||
Formspecs are also supported within a node definition. If an on_open( ) method is defined, then it will
|
||||
be invoked whenever the player right-clicks the node. The return value must be a standard formspec string,
|
||||
or nil to abort.
|
||||
|
||||
* nodedef.on_open( pos, player, fields )
|
||||
* pos - the position of the node (or your own state table)
|
||||
* player - the player object
|
||||
|
||||
The on_close( ) method is invoked during any formspec event or signal. If defined,
|
||||
it will receive three arguments. Currently, the return value is ignored, but in the
|
||||
future it may allow for additional behavior.
|
||||
|
||||
* nodedef.on_close( pos, player, fields )
|
||||
* pos - the position of the node (or your own state table)
|
||||
* player - the player object
|
||||
* fields - the table of submitted fields
|
||||
|
||||
An optional before_open( ) method may also be defined. It should return a custom
|
||||
state table to be subsequently passed to the on_open( ) method in place of the
|
||||
default "pos" parameter.
|
||||
|
||||
* nodedef.before_open( pos, node, player )
|
||||
* pos - the position of the node (or your own state table)
|
||||
* node - the node
|
||||
* player - the player object
|
||||
|
||||
The "hidden" formspec element of allows for a slightly different workflow, by
|
||||
presetting the state table from the formspec string itself. The key-value pairs
|
||||
are used only for initialization and will never be transmitted to the client.
|
||||
An optional data type can be specified within the element as follows:
|
||||
* hidden[name;value;string]
|
||||
* hidden[name;value;boolean]
|
||||
* hidden[name;value;number]
|
||||
* hidden[name;value]
|
||||
|
||||
If the data type is not specified, then no type-conversion will occur. Here are
|
||||
some examples:
|
||||
|
||||
* hidden[pos_x;-240;number]"
|
||||
* hidden[wool_color;darkgreen;string]"
|
||||
* hidden[is_dead;true;boolean]"
|
||||
|
||||
Through the use of form timers, it possible to re-open a formspec at regular intervals,
|
||||
or even to close the formspec after a specified period. There is no need for complex
|
||||
chains of minetest.after( ) or additional globalstep registrations. A form timer persists
|
||||
for the duration of a form session.
|
||||
|
||||
* minetest.get_form_timer( player_name, form_name )
|
||||
|
||||
Returns a form timer object associated with the form session of a given player.
|
||||
Three methods are exposed by the form timer object, providing similar behavior to node timers.
|
||||
* timer.start( timeout ) Starts a timer with the given timeout in seconds.
|
||||
* timer.stop( ) Cancels a running timer.
|
||||
* timer.get_state( ) Returns a table with information about the running timer
|
||||
|
||||
The table returned including:
|
||||
* elapsed - the number of seconds since the timer started
|
||||
* remain - the number of seconds until the timer expires
|
||||
* overrun - the number of milliseconds overrun for this period (only valid within an on_close( ) callback)
|
||||
* counter - the number of periods that have accrued
|
||||
|
||||
The associated on_close( ) callback function will be notified of timer expiration via a SIG_TIME signal.
|
||||
|
||||
#### Code examples
|
||||
|
||||
To better understand this methodology, here are some working code examples. Let's say that we want to register an "uptime" chat command that displays the current server uptime to any player, with an option to automatically refresh the formspec each second.
|
||||
|
||||
```
|
||||
minetest.register_chatcommand( "uptime", {
|
||||
description = "View the uptime of the server interactively",
|
||||
func = function( player_name, param )
|
||||
local is_refresh = true
|
||||
|
||||
local get_formspec = function( )
|
||||
local uptime = minetest.get_server_uptime( )
|
||||
|
||||
local formspec = "size[4,2]"
|
||||
.. string.format( "label[0.5,0.5;%s %d secs]",
|
||||
minetest.colorize( "#FFFF00", "Server Uptime:" ), uptime
|
||||
)
|
||||
.. "checkbox[0.5,1;is_refresh;Auto Refresh;" .. tostring( is_refresh ) .. "]"
|
||||
return formspec
|
||||
end
|
||||
local on_close = function( state, player, fields )
|
||||
if fields.quit == minetest.FORMSPEC_SIGTIME then
|
||||
minetest.update_form( player_name, get_formspec( ) )
|
||||
|
||||
elseif fields.is_refresh then
|
||||
is_refresh = fields.is_refresh == "true"
|
||||
if is_refresh == true then
|
||||
minetest.get_form_timer( player_name ).start( 1 )
|
||||
else
|
||||
minetest.get_form_timer( player_name ).stop( )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), on_close )
|
||||
minetest.get_form_timer( player_name ).start( 1 )
|
||||
end
|
||||
} )
|
||||
```
|
||||
|
||||
Of course, we could implement similar functionality without the need for a chat command. Perhaps we want to display the server uptime only when a privileged player clicks on a Nyan Cat while limiting the total number of refreshes to ten.
|
||||
|
||||
```
|
||||
minetest.register_privilege( "uptime", "View the uptime of the server interactively" )
|
||||
|
||||
local function open_system_monitor( player_name, is_minutes )
|
||||
local view_count = 0
|
||||
local view_limit = 10
|
||||
|
||||
local function get_formspec( )
|
||||
local uptime = minetest.get_server_uptime( )
|
||||
local formspec = "size[4,3]"
|
||||
.. string.format( "label[0.5,0.5;%s %0.1f %s]",
|
||||
minetest.colorize( "#FFFF00", "Server Uptime:" ),
|
||||
is_minutes and uptime / 60 or uptime,
|
||||
is_minutes and "mins" or "secs"
|
||||
)
|
||||
.. "checkbox[0.5,1;is_minutes;Show Minutes;" .. tostring( is_minutes ) .. "]"
|
||||
.. "button[0.5,2;2.5,1;update;Refresh]"
|
||||
.. "hidden[view_count;1;number]"
|
||||
.. "hidden[view_limit;10;number]"
|
||||
return formspec
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), function( state, player, fields )
|
||||
if not minetest.check_player_privs( player_name, "uptime" ) then -- sanity check
|
||||
return
|
||||
end
|
||||
|
||||
if fields.update then
|
||||
-- limit the number of refreshes!
|
||||
if view_count == view_limit then
|
||||
minetest.destroy_form( player_name )
|
||||
minetest.chat_send_player( player_name, "You've exceeded the refresh limit." )
|
||||
else
|
||||
view_count = view_count + 1
|
||||
minetest.update_form( player_name, get_formspec( ) )
|
||||
end
|
||||
|
||||
elseif fields.is_minutes then
|
||||
is_minutes = fields.is_minutes == "true"
|
||||
minetest.update_form( player_name, get_formspec( ) )
|
||||
end
|
||||
end )
|
||||
end
|
||||
|
||||
minetest.override_item( "nyancat:nyancat", {
|
||||
description = "System Monitor",
|
||||
|
||||
on_rightclick = function( pos, node, player )
|
||||
local player_name = player:get_player_name( )
|
||||
|
||||
if minetest.check_player_privs( player_name, "uptime" ) then
|
||||
open_system_monitor( player_name, true )
|
||||
else
|
||||
minetest.chat_send_player( player_name, "Your privileges are insufficient." )
|
||||
end
|
||||
end,
|
||||
} )
|
||||
```
|
||||
|
||||
Revision History
|
||||
----------------------
|
||||
|
||||
Version 1.0a (15-Dec-2016)
|
||||
- initial version within default mod
|
||||
|
||||
Version 1.1b (16-Dec-2016)
|
||||
- added better comments
|
||||
|
||||
Version 1.2b (18-Dec-2016)
|
||||
- renamed public methods
|
||||
|
||||
Version 1.3b (04-Jan-2017)
|
||||
- fixed logic of quit event to require valid session
|
||||
|
||||
Version 1.4b (26-Jul-2017)
|
||||
- added method to update formspec and maintain session
|
||||
|
||||
Version 2.0 (24-Dec-2017)
|
||||
- separated all routines into new mod for public release
|
||||
|
||||
Version 2.1 (08-Jan-2018)
|
||||
- various code refactoring and better comments
|
||||
- introduced password hashing of form names
|
||||
- improved sanity checks during form submission
|
||||
- fully reworked parsing of hidden elements
|
||||
- ensured hidden elements are always stripped
|
||||
- gave hidden elements default-state behavior
|
||||
- localized old node registration functions
|
||||
- included player object within form table
|
||||
- added signal handling on formspec termination
|
||||
- added support for callbacks in node overrides
|
||||
|
||||
Version 2.2 (19-Jan-2018)
|
||||
- introduced player-name arguments for all API calls
|
||||
- added signal for programmatic formspec closure
|
||||
- ensured callbacks are notified of session resets
|
||||
- renamed some local variables to improve clarity
|
||||
|
||||
Version 2.3 (28-Jan-2018)
|
||||
- corrected erroneous value of formspec exit signal
|
||||
- removed two experimental form session methods
|
||||
- included timestamp and origin within form table
|
||||
- added form session validation on destroy and update
|
||||
- introduced form timers with start and stop methods
|
||||
- created routine to notify callbacks of timeout
|
||||
- added support for internal statistical tracking
|
||||
- added chat command to view form session summary
|
||||
|
||||
Version 2.4 (12-Feb-2018)
|
||||
- various code refactoring and better comments
|
||||
- full rewrite of timer queue for higher precision
|
||||
|
||||
Version 2.5 (01-Feb-2019)
|
||||
- made callback function optional with default no-op
|
||||
- added non-trappable form session termination signal
|
||||
- properly reset timestamp for lifetime calculation
|
||||
|
||||
Version 2.6 (02-Feb-2020)
|
||||
- added callback to preset state of node formspecs
|
||||
- removed experimental property from node definition
|
||||
- implemented reverse-lookups for dropdown fields
|
||||
- extended dropdown element with optional parameter
|
||||
- added signal to suspend and restore form sessions
|
||||
- combined element parsers into dedicated function
|
||||
- added functions for escaping formspec strings
|
||||
- revamped element parsers to ignore malformed tags
|
||||
- added conditional pattern matching helper function
|
||||
- compatability bumped to Minetest 0.4.15+
|
||||
|
||||
Compatibility
|
||||
----------------------
|
||||
|
||||
Minetest 0.4.15+ required or 5.X
|
||||
|
||||
Installation
|
||||
----------------------
|
||||
|
||||
1. Unzip the archive into the mods directory or game mods directory
|
||||
2. Get sure to the name of this directory to "formspecs" (if you cloned or decompress)
|
||||
3. Add "formspecs" as a dependency to any mods using the API.
|
||||
|
||||
Source Code License
|
||||
----------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2020, Leslie E. Krause (leslie@searstower.org) aka sorceredkid
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more details:
|
||||
https://opensource.org/licenses/MIT
|
1
mods/formspecs/description.txt
Normal file
1
mods/formspecs/description.txt
Normal file
@ -0,0 +1 @@
|
||||
ActiveFormspecs is a self-contained API that provides secure session tracking, session-based state tables, and localized event handling of formspecs for individual mods as well as entire subgames
|
509
mods/formspecs/init.lua
Normal file
509
mods/formspecs/init.lua
Normal file
@ -0,0 +1,509 @@
|
||||
--------------------------------------------------------
|
||||
-- Minetest :: ActiveFormspecs Mod v2.6 (formspecs)
|
||||
--
|
||||
-- See README.txt for licensing and release notes.
|
||||
-- Copyright (c) 2016-2019, Leslie Ellen Krause
|
||||
--
|
||||
-- ./games/just_test_tribute/mods/formspecs/init.lua
|
||||
--------------------------------------------------------
|
||||
|
||||
print( "Loading ActiveFormspecs Mod" )
|
||||
|
||||
minetest.FORMSPEC_SIGEXIT = "true" -- player clicked exit button or pressed esc key (boolean for backward compatibility)
|
||||
minetest.FORMSPEC_SIGQUIT = 1 -- player logged off
|
||||
minetest.FORMSPEC_SIGKILL = 2 -- player was killed
|
||||
minetest.FORMSPEC_SIGTERM = 3 -- server is shutting down
|
||||
minetest.FORMSPEC_SIGPROC = 4 -- procedural closure
|
||||
minetest.FORMSPEC_SIGTIME = 5 -- timeout reached
|
||||
minetest.FORMSPEC_SIGSTOP = 6 -- procedural closure (cannot be trapped)
|
||||
minetest.FORMSPEC_SIGHOLD = 7 -- child form opened, parent is suspended
|
||||
minetest.FORMSPEC_SIGCONT = 8 -- child form closed, parent can continue
|
||||
|
||||
local afs = { } -- obtain localized, protected namespace
|
||||
|
||||
afs.forms = { }
|
||||
afs.timers = { }
|
||||
afs.session_id = 0
|
||||
afs.session_seed = math.random( 0, 65535 )
|
||||
|
||||
afs.stats = { active = 0, opened = 0, closed = 0 }
|
||||
|
||||
afs.stats.on_open = function ( self )
|
||||
self.active = self.active + 1
|
||||
self.opened = self.opened + 1
|
||||
end
|
||||
|
||||
afs.stats.on_close = function ( self )
|
||||
self.active = self.active - 1
|
||||
self.closed = self.closed + 1
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- trigger callbacks at set intervals within timer queue
|
||||
-----------------------------------------------------------------
|
||||
|
||||
do
|
||||
-- localize needed object references for efficiency
|
||||
local get_us_time = minetest.get_us_time
|
||||
local timers = afs.timers
|
||||
local t_cur = get_us_time( )
|
||||
local t_off = -t_cur
|
||||
|
||||
-- step monotonic clock with graceful 32-bit overflow
|
||||
local step_clock = function( )
|
||||
local t_new = get_us_time( )
|
||||
|
||||
if t_new < t_cur then
|
||||
t_off = t_off + 4294967290
|
||||
end
|
||||
|
||||
t_cur = t_new
|
||||
return t_off + t_new
|
||||
end
|
||||
afs.get_uptime = function( )
|
||||
return ( t_off + t_cur ) / 1000000
|
||||
end
|
||||
|
||||
minetest.register_globalstep( function( dtime )
|
||||
--local x = get_us_time( )
|
||||
local curtime = step_clock( ) / 1000000
|
||||
local idx = #timers
|
||||
|
||||
-- iterate through table in reverse order to allow removal
|
||||
while idx > 0 do
|
||||
local self = timers[ idx ]
|
||||
|
||||
if curtime >= self.exptime then
|
||||
self.counter = self.counter + 1
|
||||
self.overrun = curtime - self.exptime
|
||||
self.exptime = curtime + self.form.timeout
|
||||
|
||||
self.form.newtime = math.floor( curtime )
|
||||
self.form.on_close( self.form.meta, self.form.player, { quit = minetest.FORMSPEC_SIGTIME } )
|
||||
|
||||
self.overrun = 0.0
|
||||
end
|
||||
idx = idx - 1
|
||||
end
|
||||
end )
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- override node registrations for attached formspecs
|
||||
-----------------------------------------------------------------
|
||||
|
||||
local on_rightclick = function( pos, node, player )
|
||||
local nodedef = minetest.registered_nodes[ node.name ]
|
||||
local meta = nodedef.before_open and nodedef.before_open( pos, node, player ) or pos
|
||||
local formspec = nodedef.on_open( meta, player )
|
||||
|
||||
if formspec then
|
||||
local player_name = player:get_player_name( )
|
||||
minetest.create_form( meta, player_name, formspec, nodedef.on_close )
|
||||
afs.forms[ player_name ].origin = node.name
|
||||
end
|
||||
end
|
||||
|
||||
local old_register_node = minetest.register_node
|
||||
local old_override_item = minetest.override_item
|
||||
|
||||
minetest.register_node = function ( name, def )
|
||||
if def.on_open and not def.on_rightclick then
|
||||
def.on_rightclick = on_rightclick
|
||||
end
|
||||
old_register_node( name, def )
|
||||
end
|
||||
|
||||
minetest.override_item = function ( name, def )
|
||||
if minetest.registered_nodes[ name ] and def.on_open then
|
||||
def.on_rightclick = on_rightclick
|
||||
end
|
||||
old_override_item( name, def )
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- trigger callbacks during formspec events
|
||||
-----------------------------------------------------------------
|
||||
|
||||
minetest.register_on_player_receive_fields( function( player, formname, fields )
|
||||
local player_name = player:get_player_name( )
|
||||
local form = afs.forms[ player_name ]
|
||||
|
||||
-- perform a basic sanity check, since these shouldn't technically occur
|
||||
if not form or player ~= form.player or formname ~= form.name then return end
|
||||
|
||||
-- handle reverse-lookups of dropdown indexes
|
||||
for name, keys in pairs( form.dropdowns ) do
|
||||
if fields[ name ] then
|
||||
fields[ name ] = keys[ fields[ name ] ]
|
||||
end
|
||||
end
|
||||
|
||||
form.newtime = os.time( )
|
||||
form.on_close( form.meta, form.player, fields )
|
||||
|
||||
-- end current session when closing formspec
|
||||
if fields.quit then
|
||||
minetest.get_form_timer( player_name ).stop( )
|
||||
|
||||
afs.stats:on_close( )
|
||||
if form.parent_form then
|
||||
-- restore previous session
|
||||
form = form.parent_form
|
||||
afs.forms[ player_name ] = form
|
||||
|
||||
-- delay a single tick to ensure formspec updates are handled by client
|
||||
minetest.after( 0.0, function ( )
|
||||
form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGCONT } )
|
||||
end )
|
||||
else
|
||||
afs.forms[ player_name ] = nil
|
||||
end
|
||||
end
|
||||
end )
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- expose timer functionality within a helper object
|
||||
-----------------------------------------------------------------
|
||||
|
||||
minetest.get_form_timer = function ( player_name, form_name )
|
||||
local self = { }
|
||||
local form = afs.forms[ player_name ]
|
||||
|
||||
if not form or form_name and form_name ~= form.name then return end
|
||||
|
||||
self.start = function ( timeout )
|
||||
if not form.timeout and timeout >= 0.5 then
|
||||
local curtime = afs.get_uptime( )
|
||||
|
||||
form.timeout = timeout
|
||||
table.insert( afs.timers, { form = form, counter = 0, oldtime = curtime, exptime = curtime + timeout, overrun = 0.0 } )
|
||||
end
|
||||
end
|
||||
self.stop = function ( )
|
||||
if not form.timeout then return end
|
||||
|
||||
form.timeout = nil
|
||||
|
||||
for i, v in ipairs( afs.timers ) do
|
||||
if v.form == form then
|
||||
table.remove( afs.timers, i )
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
self.get_state = function ( )
|
||||
if not form.timeout then return end
|
||||
|
||||
for i, v in ipairs( afs.timers ) do
|
||||
local curtime = afs.get_uptime( )
|
||||
|
||||
if v.form == form then
|
||||
return { elapsed = curtime - v.oldtime, remain = v.exptime - curtime, overrun = v.overrun, counter = v.counter }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- parse specialized formspec elements and escapes codes
|
||||
-----------------------------------------------------------------
|
||||
|
||||
local _
|
||||
local function is_match( str, pat )
|
||||
-- use array for captures
|
||||
_ = { string.match( str, pat ) }
|
||||
return #_ > 0 and _ or nil
|
||||
end
|
||||
|
||||
local function escape( str )
|
||||
return string.gsub( str, "\\.",
|
||||
{ ["\\]"] = "\\x5D", ["\\["] = "\\x5B", ["\\,"] = "\\x2C", ["\\;"] = "\\x3B" } )
|
||||
end
|
||||
|
||||
local function unescape( str, is_raw )
|
||||
return string.gsub( str, "\\x..",
|
||||
{ ["\\x5D"] = "\\]", ["\\x5B"] = "\\[", ["\\x2C"] = "\\,", ["\\x3B"] = "\\;" } )
|
||||
end
|
||||
|
||||
local function unescape_raw( str, is_raw )
|
||||
return string.gsub( str, "\\x..",
|
||||
{ ["\\x5D"] = "]", ["\\x5B"] = "[", ["\\x2C"] = ",", ["\\x3B"] = ";" } )
|
||||
end
|
||||
|
||||
local function parse_elements( form, formspec )
|
||||
formspec = escape( formspec )
|
||||
form.dropdowns = { } -- reset the dropdown lookup
|
||||
|
||||
-- dropdown elements can optionally return the selected
|
||||
-- index rather than the value of the option itself
|
||||
formspec = string.gsub( formspec, "dropdown%[(.-)%]", function( params )
|
||||
if is_match( params, "^([^;]*;[^;]*;([^;]*);([^;]*);[^;]*);([^;]*)$" ) then
|
||||
local prefix = _[ 1 ]
|
||||
local name = _[ 2 ]
|
||||
local options = _[ 3 ]
|
||||
local use_index = _[ 4 ]
|
||||
|
||||
if use_index == "true" then
|
||||
form.dropdowns[ name ] = { }
|
||||
for idx, val in ipairs( string.split( options, ",", true ) ) do
|
||||
form.dropdowns[ name ][ unescape_raw( val ) ] = idx -- add to reverse lookup table
|
||||
end
|
||||
return string.format( "dropdown[%s]", prefix )
|
||||
elseif use_index == "false" or use_index == "" then
|
||||
return string.format( "dropdown[%s]", prefix )
|
||||
else
|
||||
return "" -- strip invalid dropdown elements
|
||||
end
|
||||
end
|
||||
return string.format( "dropdown[%s]", params )
|
||||
end )
|
||||
|
||||
-- hidden elements only provide default, initial values
|
||||
-- for state table and are always stripped afterward
|
||||
formspec = string.gsub( formspec, "hidden%[(.-)%]", function( params )
|
||||
if is_match( params, "^([^;]*);([^;]*)$" ) or is_match( params, "^([^;]*);([^;]*);([^;]*)$" ) then
|
||||
local key = _[ 1 ]
|
||||
local value = _[ 2 ]
|
||||
local type = _[ 3 ]
|
||||
|
||||
if key ~= "" and form.meta[ key ] == nil then
|
||||
-- parse according to specified data type
|
||||
if type == "string" or type == "" or type == nil then
|
||||
form.meta[ key ] = unescape_raw( value )
|
||||
elseif type == "number" then
|
||||
form.meta[ key ] = tonumber( value )
|
||||
elseif type == "boolean" then
|
||||
form.meta[ key ] = ( { ["1"] = true, ["0"] = false, ["true"] = true, ["false"] = false } )[ value ]
|
||||
end
|
||||
end
|
||||
end
|
||||
return "" -- strip hidden elements prior to showing formspec
|
||||
end )
|
||||
|
||||
return unescape( formspec )
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- open detached formspec with session-based state table
|
||||
-----------------------------------------------------------------
|
||||
|
||||
minetest.create_form = function ( meta, player_name, formspec, on_close, signal )
|
||||
-- short circuit whenever required params are missing
|
||||
if not player_name or not formspec then return end
|
||||
|
||||
if type( player_name ) ~= "string" then
|
||||
player_name = player_name:get_player_name( )
|
||||
end
|
||||
|
||||
local form = afs.forms[ player_name ]
|
||||
|
||||
-- trigger previous callback before formspec closure
|
||||
if form then
|
||||
minetest.get_form_timer( player_name, form.name ).stop( )
|
||||
if signal ~= minetest.FORMSPEC_SIGSTOP then
|
||||
form.on_close( form.meta, form.player, { quit = signal or minetest.FORMSPEC_SIGPROC } )
|
||||
end
|
||||
if signal ~= minetest.FORMSPEC_SIGHOLD then
|
||||
form = nil
|
||||
afs.stats:on_close( )
|
||||
end
|
||||
end
|
||||
|
||||
-- start new session when opening formspec
|
||||
afs.session_id = afs.session_id + 1
|
||||
|
||||
form = { parent_form = form }
|
||||
form.id = afs.session_id
|
||||
form.name = minetest.get_password_hash( player_name, afs.session_seed + afs.session_id )
|
||||
form.player = minetest.get_player_by_name( player_name )
|
||||
form.origin = string.match( debug.getinfo( 2 ).source, "^@.*[/\\]mods[/\\](.-)[/\\]" ) or "?"
|
||||
form.on_close = on_close or function ( ) end
|
||||
form.meta = meta or { }
|
||||
form.oldtime = math.floor( afs.get_uptime( ) )
|
||||
form.newtime = form.oldtime
|
||||
|
||||
afs.forms[ player_name ] = form
|
||||
afs.stats:on_open( )
|
||||
minetest.show_formspec( player_name, form.name, parse_elements( form, formspec ) )
|
||||
|
||||
return form.name
|
||||
end
|
||||
|
||||
minetest.update_form = function ( player, formspec )
|
||||
local pname = type( player ) == "string" and player or player:get_player_name( )
|
||||
local form = afs.forms[ pname ]
|
||||
|
||||
if form then
|
||||
form.oldtime = math.floor( afs.get_uptime( ) )
|
||||
minetest.show_formspec( pname, form.name, parse_elements( form, formspec ) )
|
||||
end
|
||||
end
|
||||
|
||||
minetest.destroy_form = function ( player, signal )
|
||||
local pname = type( player ) == "string" and player or player:get_player_name( )
|
||||
local form = afs.forms[ pname ]
|
||||
|
||||
if form then
|
||||
minetest.close_formspec( pname, form.name )
|
||||
minetest.get_form_timer( pname ):stop( )
|
||||
|
||||
if signal ~= minetest.FORMSPEC_SIGSTOP then
|
||||
form.on_close( form.meta, form.player, { quit = signal or minetest.FORMSPEC_SIGPROC } )
|
||||
end
|
||||
|
||||
afs.stats:on_close( )
|
||||
afs.forms[ pname ] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- trigger callbacks after unexpected formspec closure
|
||||
-----------------------------------------------------------------
|
||||
|
||||
minetest.register_on_leaveplayer( function( player, is_timeout )
|
||||
local pname = player:get_player_name( )
|
||||
local form = afs.forms[ pname ]
|
||||
|
||||
if form then
|
||||
minetest.get_form_timer( pname, form.name ).stop( )
|
||||
|
||||
form.newtime = os.time( )
|
||||
form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGQUIT } )
|
||||
|
||||
afs.stats:on_close( )
|
||||
afs.forms[ pname ] = nil
|
||||
end
|
||||
end )
|
||||
|
||||
minetest.register_on_dieplayer( function( player )
|
||||
local pname = player:get_player_name( )
|
||||
local form = afs.forms[ pname ]
|
||||
|
||||
if form then
|
||||
minetest.get_form_timer( pname, form.name ).stop( )
|
||||
|
||||
form.newtime = os.time( )
|
||||
form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGKILL } )
|
||||
|
||||
afs.stats:on_close( )
|
||||
afs.forms[ pname ] = nil
|
||||
end
|
||||
end )
|
||||
|
||||
minetest.register_on_shutdown( function( )
|
||||
for _, form in pairs( afs.forms ) do
|
||||
minetest.get_form_timer( form.player:get_player_name( ), form.name ).stop( )
|
||||
|
||||
form.newtime = os.time( )
|
||||
form.on_close( form.meta, form.player, { quit = minetest.FORMSPEC_SIGTERM } )
|
||||
|
||||
afs.stats:on_close( )
|
||||
end
|
||||
afs.forms = { }
|
||||
end )
|
||||
|
||||
-----------------------------------------------------------------
|
||||
-- display realtime information about form sessions
|
||||
-----------------------------------------------------------------
|
||||
|
||||
minetest.register_chatcommand( "fs", {
|
||||
description = "Display realtime information about form sessions",
|
||||
privs = { server = true },
|
||||
func = function( pname, param )
|
||||
local page_idx = 1
|
||||
local page_size = 10
|
||||
local sorted_forms
|
||||
|
||||
local get_sorted_forms = function( )
|
||||
local f = { }
|
||||
for k, v in pairs( afs.forms ) do
|
||||
table.insert( f, v )
|
||||
end
|
||||
table.sort( f, function( a, b ) return a.id < b.id end )
|
||||
return f
|
||||
end
|
||||
local get_formspec = function( )
|
||||
local uptime = math.floor( afs.get_uptime( ) )
|
||||
|
||||
local formspec = "size[9.5,7.5]"
|
||||
.. default.gui_bg
|
||||
.. default.gui_bg_img
|
||||
|
||||
.. "label[0.1,6.7;ActiveFormspecs v2.6"
|
||||
.. string.format( "label[0.1,0.0;%s]label[0.1,0.5;%d min %02d sec]",
|
||||
minetest.colorize( "#888888", "uptime:" ), math.floor( uptime / 60 ), uptime % 60 )
|
||||
.. string.format( "label[5.6,0.0;%s]label[5.6,0.5;%d]",
|
||||
minetest.colorize( "#888888", "active" ), afs.stats.active )
|
||||
.. string.format( "label[6.9,0.0;%s]label[6.9,0.5;%d]",
|
||||
minetest.colorize( "#888888", "opened" ), afs.stats.opened )
|
||||
.. string.format( "label[8.2,0.0;%s]label[8.2,0.5;%d]",
|
||||
minetest.colorize( "#888888", "closed" ), afs.stats.closed )
|
||||
|
||||
.. string.format( "label[0.5,1.5;%s]label[3.5,1.5;%s]label[6.9,1.5;%s]label[8.2,1.5;%s]",
|
||||
minetest.colorize( "#888888", "player" ),
|
||||
minetest.colorize( "#888888", "origin" ),
|
||||
minetest.colorize( "#888888", "idletime" ),
|
||||
minetest.colorize( "#888888", "lifetime" )
|
||||
)
|
||||
|
||||
.. "box[0,1.2;9.2,0.1;#111111]"
|
||||
.. "box[0,6.2;9.2,0.1;#111111]"
|
||||
|
||||
local num = 0
|
||||
for idx = ( page_idx - 1 ) * page_size + 1, math.min( page_idx * page_size, #sorted_forms ) do
|
||||
local form = sorted_forms[ idx ]
|
||||
|
||||
local player_name = form.player:get_player_name( )
|
||||
local lifetime = uptime - form.oldtime
|
||||
local idletime = uptime - form.newtime
|
||||
|
||||
local vert = 2.0 + num * 0.5
|
||||
|
||||
formspec = formspec
|
||||
.. string.format( "button[0.1,%0.1f;0.5,0.3;del:%s;x]", vert + 0.1, player_name )
|
||||
.. string.format( "label[0.5,%0.1f;%s]", vert, player_name )
|
||||
.. string.format( "label[3.5,%0.1f;%s]", vert, form.origin )
|
||||
.. string.format( "label[6.9,%0.1f;%dm %02ds]", vert, math.floor( idletime / 60 ), idletime % 60 )
|
||||
.. string.format( "label[8.2,%0.1f;%dm %02ds]", vert, math.floor( lifetime / 60 ), lifetime % 60 )
|
||||
num = num + 1
|
||||
end
|
||||
|
||||
formspec = formspec
|
||||
.. "button[6.4,6.5;1,1;prev;<<]"
|
||||
.. string.format( "label[7.4,6.7;%d of %d]", page_idx, math.max( 1, math.ceil( #sorted_forms / page_size ) ) )
|
||||
.. "button[8.4,6.5;1,1;next;>>]"
|
||||
|
||||
return formspec
|
||||
end
|
||||
local on_close = function( meta, player, fields )
|
||||
if fields.quit == minetest.FORMSPEC_SIGTIME then
|
||||
sorted_forms = get_sorted_forms( )
|
||||
minetest.update_form( pname, get_formspec( ) )
|
||||
|
||||
elseif fields.prev and page_idx > 1 then
|
||||
page_idx = page_idx - 1
|
||||
minetest.update_form( pname, get_formspec( ) )
|
||||
|
||||
elseif fields.next and page_idx < #sorted_forms / page_size then
|
||||
page_idx = page_idx + 1
|
||||
minetest.update_form( pname, get_formspec( ) )
|
||||
|
||||
else
|
||||
local player_name = string.match( next( fields, nil ), "del:(.+)" )
|
||||
if player_name and afs.forms[ player_name ] then
|
||||
minetest.destroy_form( player_name )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sorted_forms = get_sorted_forms( )
|
||||
|
||||
minetest.create_form( nil, pname, get_formspec( ), on_close )
|
||||
minetest.get_form_timer( pname ).start( 1 )
|
||||
|
||||
return true
|
||||
end,
|
||||
} )
|
5
mods/formspecs/mod.conf
Normal file
5
mods/formspecs/mod.conf
Normal file
@ -0,0 +1,5 @@
|
||||
name = formspecs
|
||||
title = ActiveFormspecs
|
||||
author = sorcerykid
|
||||
license = MIT
|
||||
description = ActiveFormspecs is a self-contained API that provides secure session tracking, session-based state tables, and localized event handling of formspecs for individual mods as well as entire subgames
|
96
mods/formspecs/samples.lua
Normal file
96
mods/formspecs/samples.lua
Normal file
@ -0,0 +1,96 @@
|
||||
-------------------------------------------------------------------------------------------
|
||||
-- How to try this example:
|
||||
-- 1) Move this file into a new "afs_test" directory under mods and rename it "init.lua".
|
||||
-- 2) Create a "depends.txt" file in the new directory with the following lines of text:
|
||||
-- nyancat
|
||||
-- formspecs
|
||||
-- 3) Launch your Minetest server and enable the "afs_test" mod. Then, login as usual!
|
||||
-------------------------------------------------------------------------------------------
|
||||
|
||||
minetest.register_privilege( "uptime", "View the uptime of the server interactively" )
|
||||
|
||||
local get_nyancat_formspec = function( meta )
|
||||
local uptime = minetest.get_server_uptime( )
|
||||
local formspec = "size[4,3]"
|
||||
.. default.gui_bg_img
|
||||
.. string.format( "label[0.5,0.5;%s %0.1f %s]",
|
||||
minetest.colorize( "#FFFF00", "Server Uptime:" ),
|
||||
meta.is_minutes == true and uptime / 60 or uptime,
|
||||
meta.is_minutes == true and "mins" or "secs"
|
||||
)
|
||||
.. "checkbox[0.5,1;is_minutes;Show Minutes;" .. tostring( meta.is_minutes ) .. "]"
|
||||
.. "button[0.5,2;2.5,1;update;Refresh]"
|
||||
.. "hidden[view_count;1;number]"
|
||||
.. "hidden[view_limit;10;number]" -- limit the number of refreshes!
|
||||
return formspec
|
||||
end
|
||||
|
||||
minetest.override_item( "nyancat:nyancat", {
|
||||
description = "System Monitor",
|
||||
|
||||
on_open = function( meta, player )
|
||||
local player_name = player:get_player_name( )
|
||||
|
||||
if meta.is_minutes == nil then meta.is_minutes = true end
|
||||
|
||||
if minetest.check_player_privs( player, "uptime" ) then
|
||||
return get_nyancat_formspec( meta )
|
||||
else
|
||||
minetest.chat_send_player( player_name, "Your privileges are insufficient." )
|
||||
end
|
||||
end,
|
||||
on_close = function( meta, player, fields )
|
||||
local player_name = player:get_player_name( )
|
||||
|
||||
if not minetest.check_player_privs( player, "uptime" ) then return end
|
||||
|
||||
if fields.update then
|
||||
if meta.view_count == meta.view_limit then
|
||||
minetest.destroy_form( player_name )
|
||||
minetest.chat_send_player( player_name, "You've exceeded the refresh limit." )
|
||||
else
|
||||
meta.view_count = meta.view_count + 1
|
||||
minetest.update_form( player_name, get_nyancat_formspec( meta ) )
|
||||
end
|
||||
|
||||
elseif fields.is_minutes then
|
||||
meta.is_minutes = fields.is_minutes == "true"
|
||||
minetest.update_form( player_name, get_nyancat_formspec( meta ) )
|
||||
end
|
||||
end
|
||||
} )
|
||||
|
||||
minetest.register_chatcommand( "uptime", {
|
||||
description = "View the uptime of the server interactively",
|
||||
func = function( player_name, param )
|
||||
local is_refresh = true
|
||||
|
||||
local get_formspec = function( )
|
||||
local uptime = minetest.get_server_uptime( )
|
||||
|
||||
local formspec = "size[4,2]"
|
||||
.. default.gui_bg_img
|
||||
.. string.format( "label[0.5,0.5;%s %d secs]",
|
||||
minetest.colorize( "#FFFF00", "Server Uptime:" ), uptime
|
||||
)
|
||||
.. "checkbox[0.5,1;is_refresh;Auto Refresh;" .. tostring( is_refresh ) .. "]"
|
||||
return formspec
|
||||
end
|
||||
local on_close = function( meta, player, fields )
|
||||
if fields.quit == minetest.FORMSPEC_SIGTIME then
|
||||
minetest.update_form( player_name, get_formspec( ) )
|
||||
|
||||
elseif fields.is_refresh then
|
||||
is_refresh = fields.is_refresh == "true"
|
||||
if is_refresh == true then
|
||||
minetest.get_form_timer( player_name ).start( 1 )
|
||||
else
|
||||
minetest.get_form_timer( player_name ).stop( )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
minetest.create_form( nil, player_name, get_formspec( ), on_close )
|
||||
minetest.get_form_timer( player_name ).start( 1 )
|
||||
end
|
||||
} )
|
BIN
mods/formspecs/screenshot.png
Normal file
BIN
mods/formspecs/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
Loading…
x
Reference in New Issue
Block a user