Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
14KB

  1. --------------------------------------------------------
  2. -- Minetest :: Auth Redux Mod v2.10 (auth_rx)
  3. --
  4. -- See README.txt for licensing and release notes.
  5. -- Copyright (c) 2017-2018, Leslie E. Krause
  6. --------------------------------------------------------
  7. -----------------------------------------------------
  8. -- Registered Chat Commands
  9. -----------------------------------------------------
  10. local auth_db, auth_filter -- imported
  11. minetest.register_chatcommand( "filter", {
  12. description = "Enable or disable ruleset-based login filtering, or reload a ruleset definition.",
  13. privs = { server = true },
  14. func = function( name, param )
  15. if param == "" then
  16. return true, "Login filtering is currently " .. ( auth_filter.is_active( ) and "enabled" or "disabled" ) .. "."
  17. elseif param == "disable" then
  18. auth_filter.disable( )
  19. minetest.log( "action", "Login filtering disabled by " .. name .. "." )
  20. return true, "Login filtering is disabled."
  21. elseif param == "enable" then
  22. auth_filter.enable( )
  23. minetest.log( "action", "Login filtering enabled by " .. name .. "." )
  24. return true, "Login filtering is enabled."
  25. elseif param == "reload" then
  26. auth_filter.refresh( )
  27. return true, "Ruleset definition was loaded successfully."
  28. else
  29. return false, "Unknown parameter specified."
  30. end
  31. end
  32. } )
  33. minetest.register_chatcommand( "fdebug", {
  34. description = "Start an interactive debugger for testing ruleset definitions.",
  35. privs = { server = true },
  36. func = function( name, param )
  37. if not minetest.create_form then return false, "This feature is not supported." end
  38. local epoch = os.time( { year = 1970, month = 1, day = 1, hour = 0 } )
  39. local vars = {
  40. __debug = { type = FILTER_TYPE_NUMBER, value = 0 },
  41. name = { type = FILTER_TYPE_STRING, value = "singleplayer" },
  42. addr = { type = FILTER_TYPE_ADDRESS, value = convert_ipv4( "127.0.0.1" ) },
  43. is_new = { type = FILTER_TYPE_BOOLEAN, value = true },
  44. privs_list = { type = FILTER_TYPE_SERIES, value = { } },
  45. users_list = { type = FILTER_TYPE_SERIES, is_auto = true },
  46. cur_users = { type = FILTER_TYPE_NUMBER, is_auto = true },
  47. max_users = { type = FILTER_TYPE_NUMBER, value = get_minetest_config( "max_users" ) },
  48. lifetime = { type = FILTER_TYPE_PERIOD, value = 0 },
  49. sessions = { type = FILTER_TYPE_NUMBER, value = 0 },
  50. failures = { type = FILTER_TYPE_NUMBER, value = 0 },
  51. attempts = { type = FILTER_TYPE_NUMBER, value = 0 },
  52. owner = { type = FILTER_TYPE_STRING, value = get_minetest_config( "name" ) },
  53. uptime = { type = FILTER_TYPE_PERIOD, is_auto = true },
  54. oldlogin = { type = FILTER_TYPE_MOMENT, value = epoch },
  55. newlogin = { type = FILTER_TYPE_MOMENT, value = epoch },
  56. ip_names_list = { type = FILTER_TYPE_SERIES, value = { } },
  57. ip_prelogin = { type = FILTER_TYPE_MOMENT, value = epoch },
  58. ip_oldcheck = { type = FILTER_TYPE_MOMENT, value = epoch },
  59. ip_newcheck = { type = FILTER_TYPE_MOMENT, value = epoch },
  60. ip_failures = { type = FILTER_TYPE_NUMBER, value = 0 },
  61. ip_attempts = { type = FILTER_TYPE_NUMBER, value = 0 }
  62. }
  63. 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" }
  64. 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" }
  65. local has_prompt = true
  66. local has_output = true
  67. local login_index = 2
  68. local var_index = 1
  69. local temp_file = io.open( minetest.get_worldpath( ) .. "/~greenlist.mt", "w" ):close( )
  70. local temp_filter = AuthFilter( minetest.get_worldpath( ), "~greenlist.mt", function ( err, num )
  71. return num, "The server encountered an internal error.", err
  72. end )
  73. local function clear_prompts( buffer, has_single )
  74. -- clear debug prompts from source code
  75. return string.gsub( buffer, "\n# ====== .- ======\n", "\n", has_single and 1 or nil )
  76. end
  77. local function insert_prompt( buffer, num, err )
  78. -- insert debug prompts into source code
  79. local i = 0
  80. return string.gsub( buffer, "\n", function ( )
  81. i = i + 1
  82. return ( i == num and string.format( "\n# ====== ^ Line %d: %s ^ ======\n", num, err ) or "\n" )
  83. end )
  84. end
  85. local function format_value( value, type )
  86. -- convert values to a human-readable format
  87. if type == FILTER_TYPE_STRING then
  88. return "\"" .. value .. "\""
  89. elseif type == FILTER_TYPE_NUMBER then
  90. return tostring( value )
  91. elseif type == FILTER_TYPE_BOOLEAN then
  92. return "$" .. tostring( value )
  93. elseif type == FILTER_TYPE_PERIOD then
  94. return tostring( math.abs( value ) ) .. "s"
  95. elseif type == FILTER_TYPE_MOMENT then
  96. return "+" .. tostring( value - vars.epoch.value ) .. "s"
  97. elseif type == FILTER_TYPE_ADDRESS then
  98. return table.concat( unpack_address( value ), "." )
  99. elseif type == FILTER_TYPE_SERIES then
  100. return "(" .. string.gsub( table.concat( value, "," ), "[^,]+", "\"%1\"" ) .. ")"
  101. end
  102. end
  103. local function update_vars( )
  104. -- automatically update preset variables
  105. if vars.uptime.is_auto then
  106. vars.uptime.value = minetest.get_server_uptime( ) end
  107. if vars.clock.is_auto then
  108. vars.clock.value = os.time( ) end
  109. if vars.users_list.is_auto then
  110. vars.users_list.value = auth_db.search( true ) end
  111. if vars.cur_users.is_auto then
  112. vars.cur_users.value = #auth_db.search( true ) end
  113. end
  114. local function get_formspec( buffer, status, var_state )
  115. local var_name = vars_list[ var_index ]
  116. local var_type = vars[ var_name ].type
  117. local var_value = vars[ var_name ].value
  118. local var_is_auto = vars[ var_name ].is_auto
  119. local formspec = "size[13.5,8.5]"
  120. .. default.gui_bg
  121. .. default.gui_bg_img
  122. .. "label[0.1,0.0;Ruleset Definition:]"
  123. .. "checkbox[2.6,-0.2;has_output;Show Client Output;" .. tostring( has_output ) .. "]"
  124. .. "checkbox[5.6,-0.2;has_prompt;Show Debug Prompt;" .. tostring( has_prompt ) .. "]"
  125. .. "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 ) .. "]"
  126. .. "button[0.1,7.8;2,1;export_ruleset;Save]"
  127. .. "button[2.0,7.8;2,1;import_ruleset;Load]"
  128. .. "button[4.0,7.8;2,1;process_ruleset;Process]"
  129. .. "dropdown[6,7.9;2.6,1;login_mode;Normal,New Account,Wrong Password;" .. login_index .. "]"
  130. .. "label[9.0,0.0;Preset Variables:]"
  131. .. "textlist[9.0,0.5;4,4.7;vars_list"
  132. for i, v in pairs( vars_list ) do
  133. formspec = formspec .. ( i == 1 and ";" or "," ) .. minetest.formspec_escape( v .. " = " .. format_value( vars[ v ].value, vars[ v ].type ) )
  134. end
  135. formspec = formspec .. string.format( ";%d;false]", var_index )
  136. .. "label[9.0,5.4;Name:]"
  137. .. "label[9.0,5.9;Type:]"
  138. .. string.format( "label[10.5,5.4;%s]", minetest.colorize( "#BBFF77", "$" .. var_name ) )
  139. .. string.format( "label[10.5,5.9;%s]", datatypes[ var_type ] )
  140. .. "label[9.0,6.4;Value:]"
  141. .. "field[9.2,7.5;4.3,0.25;var_value;;" .. minetest.formspec_escape( format_value( var_value, var_type ) ) .. "]"
  142. .. "button[9.0,7.8;1,1;prev_var;<<]"
  143. .. "button[10.0,7.8;1,1;next_var;>>]"
  144. .. "button[11.8,7.8;1.5,1;set_var;Set]"
  145. if var_is_auto ~= nil then
  146. formspec = formspec .. "checkbox[10.5,6.2;var_is_auto;Auto Update;" .. tostring( var_is_auto ) .. "]"
  147. end
  148. if status then
  149. formspec = formspec .. "box[0.1,6.9;8.4,0.8;#555555]"
  150. .. "label[0.3,7.1;" .. minetest.colorize( status.type == "ERROR" and "#CCCC22" or "#22CC22", status.type .. ": " ) .. status.desc .. "]"
  151. if status.user then
  152. formspec = formspec .. "textlist[0.1,5.5;8.4,1.2;;Access denied. Reason: " .. minetest.formspec_escape( status.user ) .. ";0;false]"
  153. end
  154. end
  155. return formspec
  156. end
  157. local function on_close( meta, player, fields )
  158. login_index = ( { ["Normal"] = 1, ["New Account"] = 2, ["Wrong Password"] = 3 } )[ fields.login_mode ] or 1 -- sanity check
  159. if fields.quit then
  160. os.remove( minetest.get_worldpath( ) .. "/~greenlist.mt" )
  161. elseif fields.vars_list then
  162. local event = minetest.explode_textlist_event( fields.vars_list )
  163. if event.type == "CHG" then
  164. var_index = event.index
  165. minetest.update_form( name, get_formspec( fields.buffer ) )
  166. end
  167. elseif fields.has_prompt then
  168. has_prompt = fields.has_prompt == "true"
  169. elseif fields.has_output then
  170. has_output = fields.has_output == "true"
  171. elseif fields.export_ruleset then
  172. local buffer = clear_prompts( fields.buffer .. "\n", true )
  173. local file = io.open( minetest.get_worldpath( ) .. "/greenlist.mt", "w" )
  174. if not file then
  175. error( "Cannot write to ruleset definition file." )
  176. end
  177. file:write( buffer )
  178. file:close( )
  179. minetest.update_form( name, get_formspec( buffer, { type = "ACTION", desc = "Ruleset definition exported." } ) )
  180. elseif fields.import_ruleset then
  181. local file = io.open( minetest.get_worldpath( ) .. "/greenlist.mt", "r" )
  182. if not file then
  183. error( "Cannot read from ruleset definition file." )
  184. end
  185. minetest.update_form( name, get_formspec( file:read( "*a" ), { type = "ACTION", desc = "Ruleset definition imported." } ) )
  186. file:close( )
  187. elseif fields.process_ruleset then
  188. local status
  189. local buffer = clear_prompts( fields.buffer .. "\n", true ) -- we need a trailing newline, or things will break
  190. -- output ruleset to temp file for processing
  191. local temp_file = io.open( minetest.get_worldpath( ) .. "/~greenlist.mt", "w" )
  192. temp_file:write( buffer )
  193. temp_file:close( )
  194. temp_filter.refresh( )
  195. update_vars( )
  196. if fields.login_mode == "New Account" then
  197. vars.is_new.value = true
  198. vars.privs_list.value = { }
  199. vars.lifetime.value = 0
  200. vars.sessions.value = 0
  201. vars.failures.value = 0
  202. vars.attempts.value = 0
  203. vars.newlogin.value = epoch
  204. vars.oldlogin.value = epoch
  205. else
  206. vars.is_new.value = false
  207. vars.attempts.value = vars.attempts.value + 1
  208. end
  209. -- process ruleset and benchmark performance
  210. local t = minetest.get_us_time( )
  211. local num, res, err = temp_filter.process( vars )
  212. t = ( minetest.get_us_time( ) - t ) / 1000
  213. if err then
  214. if has_prompt then buffer = insert_prompt( buffer, num, err ) end
  215. status = { type = "ERROR", desc = string.format( "%s (line %d).", err, num ), user = has_output and res }
  216. vars.ip_attempts.value = vars.ip_attempts.value + 1
  217. vars.ip_prelogin.value = vars.clock.value
  218. table.insert( vars.ip_names_list.value, vars.name.value )
  219. elseif res then
  220. if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset failed" ) end
  221. status = { type = "ACTION", desc = string.format( "Ruleset failed at line %d (took %0.1f ms).", num, t ), user = has_output and res }
  222. vars.ip_attempts.value = vars.ip_attempts.value + 1
  223. vars.ip_prelogin.value = vars.clock.value
  224. table.insert( vars.ip_names_list.value, vars.name.value )
  225. elseif fields.login_mode == "Wrong Password" then
  226. if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset failed" ) end
  227. status = { type = "ACTION", desc = string.format( "Ruleset failed at line %d (took %0.1f ms).", num, t ), user = has_output and "Invalid password" }
  228. vars.failures.value = vars.failures.value + 1
  229. vars.ip_attempts.value = vars.ip_attempts.value + 1
  230. vars.ip_failures.value = vars.ip_failures.value + 1
  231. vars.ip_prelogin.value = vars.clock.value
  232. vars.ip_newcheck.value = vars.clock.value
  233. if vars.ip_oldcheck.value == epoch then
  234. vars.ip_oldcheck.value = vars.clock.value
  235. end
  236. table.insert( vars.ip_names_list.value, vars.name.value )
  237. else
  238. if has_prompt then buffer = insert_prompt( buffer, num, "Ruleset passed" ) end
  239. status = { type = "ACTION", desc = string.format( "Ruleset passed at line %d (took %0.1f ms).", num, t ) }
  240. if fields.login_mode == "New Account" then
  241. vars.privs_list.value = get_default_privs( )
  242. end
  243. vars.sessions.value = vars.sessions.value + 1
  244. vars.newlogin.value = vars.clock.value
  245. if vars.oldlogin.value == epoch then
  246. vars.oldlogin.value = vars.clock.value
  247. end
  248. vars.ip_failures.value = 0
  249. vars.ip_attempts.value = 0
  250. vars.ip_prelogin.value = epoch
  251. vars.ip_oldcheck.value = epoch
  252. vars.ip_newcheck.value = epoch
  253. vars.ip_names_list.value = { }
  254. end
  255. minetest.update_form( name, get_formspec( buffer, status ) )
  256. elseif fields.next_var or fields.prev_var then
  257. local idx = var_index
  258. local off = fields.next_var and 1 or -1
  259. if off == 1 and idx < #vars_list or off == -1 and idx > 1 then
  260. local v = vars_list[ idx ]
  261. vars_list[ idx ] = vars_list[ idx + off ]
  262. vars_list[ idx + off ] = v
  263. var_index = idx + off
  264. minetest.update_form( name, get_formspec( fields.buffer ) )
  265. end
  266. elseif fields.var_is_auto then
  267. local var_name = vars_list[ var_index ]
  268. vars[ var_name ].is_auto = ( fields.var_is_auto == "true" )
  269. elseif fields.set_var then
  270. local oper = temp_filter.translate( string.trim( fields.var_value ), vars )
  271. local var_name = vars_list[ var_index ]
  272. if oper and var_name == "__debug" and datatypes[ oper.type ] then
  273. -- debug variable can be any value/type
  274. vars.__debug = oper
  275. elseif oper and oper.type == vars[ var_name ].type then
  276. vars[ var_name ].value = oper.value
  277. end
  278. minetest.update_form( name, get_formspec( fields.buffer ) )
  279. end
  280. end
  281. temp_filter.add_preset_vars( vars )
  282. vars.clock.is_auto = true
  283. update_vars( )
  284. minetest.create_form( nil, name, get_formspec( "pass now\n" ), on_close )
  285. return true
  286. end,
  287. } )
  288. return function ( import )
  289. auth_db = import.auth_db
  290. auth_filter = import.auth_filter
  291. end