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.

db.lua 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. --------------------------------------------------------
  2. -- Minetest :: Auth Redux Mod v2.6 (auth_rx)
  3. --
  4. -- See README.txt for licensing and release notes.
  5. -- Copyright (c) 2017-2018, Leslie E. Krause
  6. --------------------------------------------------------
  7. ----------------------------
  8. -- Transaction Op Codes
  9. ----------------------------
  10. local LOG_STARTED = 10 -- <timestamp> 10
  11. local LOG_CHECKED = 11 -- <timestamp> 11
  12. local LOG_STOPPED = 12 -- <timestamp> 12
  13. local TX_CREATE = 20 -- <timestamp> 20 <username> <password>
  14. local TX_DELETE = 21 -- <timestamp> 21 <username>
  15. local TX_SET_PASSWORD = 40 -- <timestamp> 40 <username> <password>
  16. local TX_SET_APPROVED_ADDRS = 41 -- <timestamp> 41 <username> <approved_addrs>
  17. local TX_SET_ASSIGNED_PRIVS = 42 -- <timestamp> 42 <username> <assigned_privs>
  18. local TX_SESSION_OPENED = 50 -- <timestamp> 50 <username>
  19. local TX_SESSION_CLOSED = 51 -- <timestamp> 51 <username>
  20. local TX_LOGIN_ATTEMPT = 30 -- <timestamp> 30 <username> <ip>
  21. local TX_LOGIN_FAILURE = 31 -- <timestamp> 31 <username> <ip>
  22. local TX_LOGIN_SUCCESS = 32 -- <timestamp> 32 <username>
  23. ----------------------------
  24. -- Journal Class
  25. ----------------------------
  26. function Journal( path, name, is_rollback )
  27. local file, err = io.open( path .. "/" .. name, "r+b" )
  28. local self = { }
  29. local cursor = 0
  30. local rtime = 1.0
  31. -- TODO: Verify integrity of database index
  32. if not file then
  33. minetest.log( "error", "Cannot open " .. path .. "/" .. name .. " for writing." )
  34. error( "Fatal exception in Journal( ), aborting." )
  35. end
  36. self.audit = function ( update_proc, is_rollback )
  37. -- Advance to the last set of noncommitted transactions (if any)
  38. if not is_rollback then
  39. minetest.log( "action", "Advancing database transaction log...." )
  40. for line in file:lines( ) do
  41. local fields = string.split( line, " ", true )
  42. if tonumber( fields[ 2 ] ) == LOG_STOPPED then
  43. cursor = file:seek( )
  44. end
  45. end
  46. file:seek( "set", cursor )
  47. end
  48. -- Update the database with all noncommitted transactions
  49. local meta = { }
  50. minetest.log( "action", "Replaying database transaction log...." )
  51. for line in file:lines( ) do
  52. local fields = string.split( line, " ", true )
  53. local optime = tonumber( fields[ 1 ] )
  54. local opcode = tonumber( fields[ 2 ] )
  55. update_proc( meta, optime, opcode, select( 3, unpack( fields ) ) )
  56. if opcode == LOG_CHECKED then
  57. -- Perform the commit and reset the log, if successful
  58. minetest.log( "action", "Resetting database transaction log..." )
  59. file:seek( "set", cursor )
  60. file:write( optime .. " " .. LOG_STOPPED .. "\n" )
  61. return optime
  62. end
  63. cursor = file:seek( )
  64. end
  65. end
  66. self.start = function ( )
  67. self.optime = os.time( )
  68. file:seek( "end", 0 )
  69. file:write( self.optime .. " " .. LOG_STARTED .. "\n" )
  70. cursor = file:seek( )
  71. file:write( self.optime .. " " .. LOG_CHECKED .. "\n" )
  72. end
  73. self.reset = function ( )
  74. file:seek( "set", cursor )
  75. file:write( self.optime .. " " .. LOG_STOPPED .. "\n" )
  76. self.optime = nil
  77. end
  78. self.record_raw = function ( opcode, ... )
  79. file:seek( "set", cursor )
  80. file:write( table.concat( { self.optime, opcode, ... }, " " ) .. "\n" )
  81. cursor = file:seek( )
  82. file:write( self.optime .. " " .. LOG_CHECKED .. "\n" )
  83. end
  84. minetest.register_globalstep( function( dtime )
  85. rtime = rtime - dtime
  86. if rtime <= 0.0 then
  87. if self.optime then
  88. -- touch file every 1.0 secs so we know if/when server crashes
  89. self.optime = os.time( )
  90. file:seek( "set", cursor )
  91. file:write( self.optime .. " " .. LOG_CHECKED .. "\n" )
  92. end
  93. rtime = 1.0
  94. end
  95. end )
  96. return self
  97. end
  98. ----------------------------
  99. -- AuthDatabase Class
  100. ----------------------------
  101. function AuthDatabase( path, name )
  102. local data, users, index
  103. local self = { }
  104. local journal = Journal( path, name .. "x" )
  105. -- Private methods
  106. local db_update = function( meta, optime, opcode, ... )
  107. local fields = { ... }
  108. if opcode == TX_CREATE then
  109. local rec =
  110. {
  111. password = fields[ 2 ],
  112. oldlogin = -1,
  113. newlogin = -1,
  114. lifetime = 0,
  115. total_sessions = 0,
  116. total_attempts = 0,
  117. total_failures = 0,
  118. approved_addrs = { },
  119. assigned_privs = { },
  120. }
  121. data[ fields[ 1 ] ] = rec
  122. elseif opcode == TX_DELETE then
  123. data[ fields[ 1 ] ] = nil
  124. elseif opcode == TX_SET_PASSWORD then
  125. data[ fields[ 1 ] ].password = fields[ 2 ]
  126. elseif opcode == TX_SET_APPROVED_ADDRS then
  127. data[ fields[ 1 ] ].filered_addrs = string.split( fields[ 2 ], ",", true )
  128. elseif opcode == TX_SET_ASSIGNED_PRIVS then
  129. data[ fields[ 1 ] ].assigned_privs = string.split( fields[ 2 ], ",", true )
  130. elseif opcode == TX_LOGIN_ATTEMPT then
  131. data[ fields[ 1 ] ].total_attempts = data[ fields[ 1 ] ].total_attempts + 1
  132. elseif opcode == TX_LOGIN_FAILURE then
  133. data[ fields[ 1 ] ].total_failures = data[ fields[ 1 ] ].total_failures + 1
  134. elseif opcode == TX_LOGIN_SUCCESS then
  135. if data[ fields[ 1 ] ].oldlogin == -1 then
  136. data[ fields[ 1 ] ].oldlogin = optime
  137. end
  138. meta.users[ fields[ 1 ] ] = data[ fields[ 1 ] ].newlogin
  139. data[ fields[ 1 ] ].newlogin = optime
  140. elseif opcode == TX_SESSION_OPENED then
  141. data[ fields[ 1 ] ].total_sessions = data[ fields[ 1 ] ].total_sessions + 1
  142. elseif opcode == TX_SESSION_CLOSED then
  143. data[ fields[ 1 ] ].lifetime = data[ fields[ 1 ] ].lifetime + ( optime - data[ fields[ 1 ] ].newlogin )
  144. meta.users[ fields[ 1 ] ] = nil
  145. elseif opcode == LOG_STARTED then
  146. meta.users = { }
  147. elseif opcode == LOG_CHECKED or opcode == LOG_STOPPED then
  148. -- calculate leftover session lengths due to abnormal server termination
  149. for u, t in pairs( meta.users ) do
  150. data[ u ].lifetime = data[ u ].lifetime + ( optime - data[ u ].newlogin )
  151. end
  152. meta.users = nil
  153. end
  154. end
  155. local db_reload = function ( )
  156. minetest.log( "action", "Reading authentication data from disk..." )
  157. local file, errmsg = io.open( path .. "/" .. name, "r+b" )
  158. if not file then
  159. minetest.log( "error", "Cannot open " .. path .. "/" .. name .. " for reading." )
  160. error( "Fatal exception in AuthDatabase:db_reload( ), aborting." )
  161. end
  162. local head = assert( file:read( "*line" ) )
  163. index = tonumber( string.match( head, "^auth_rx/2.1 @(%d+)$" ) )
  164. if not index or index < 0 then
  165. minetest.log( "error", "Invalid header in authentication database." )
  166. error( "Fatal exception in AuthDatabase:reload( ), aborting." )
  167. end
  168. for line in file:lines( ) do
  169. if line ~= "" then
  170. local fields = string.split( line, ":", true )
  171. if #fields ~= 10 then
  172. minetest.log( "error", "Invalid record in authentication database." )
  173. error( "Fatal exception in AuthDatabase:reload( ), aborting." )
  174. end
  175. data[ fields[ 1 ] ] = {
  176. password = fields[ 2 ],
  177. oldlogin = tonumber( fields[ 3 ] ),
  178. newlogin = tonumber( fields[ 4 ] ),
  179. lifetime = tonumber( fields[ 5 ] ),
  180. total_sessions = tonumber( fields[ 6 ] ),
  181. total_attempts = tonumber( fields[ 7 ] ),
  182. total_failures = tonumber( fields[ 8 ] ),
  183. approved_addrs = string.split( fields[ 9 ], "," ),
  184. assigned_privs = string.split( fields[ 10 ], "," ),
  185. }
  186. end
  187. end
  188. file:close( )
  189. end
  190. local db_commit = function ( )
  191. minetest.log( "action", "Writing authentication data to disk..." )
  192. local file, errmsg = io.open( path .. "/~" .. name, "w+b" )
  193. if not file then
  194. minetest.log( "error", "Cannot open " .. path .. "/~" .. name .. " for writing." )
  195. error( "Fatal exception in AuthDatabase:db_commit( ), aborting." )
  196. end
  197. index = index + 1
  198. file:write( "auth_rx/2.1 @" .. index .. "\n" )
  199. for username, rec in pairs( data ) do
  200. assert( file:write( table.concat( {
  201. username,
  202. rec.password,
  203. rec.oldlogin,
  204. rec.newlogin,
  205. rec.lifetime,
  206. rec.total_sessions,
  207. rec.total_attempts,
  208. rec.total_failures,
  209. table.concat( rec.approved_addrs, "," ),
  210. table.concat( rec.assigned_privs, "," ),
  211. }, ":" ) .. "\n" ) )
  212. end
  213. file:close( )
  214. assert( os.remove( path .. "/" .. name ) )
  215. assert( os.rename( path .. "/~" .. name, path .. "/" .. name ) )
  216. end
  217. -- Public methods
  218. self.rollback = function ( )
  219. data = { }
  220. db_reload( )
  221. journal.audit( db_update, true )
  222. db_commit( )
  223. data = nil
  224. end
  225. self.connect = function ( )
  226. data = { }
  227. users = { }
  228. db_reload( )
  229. if journal.audit( db_update, false ) then
  230. db_commit( )
  231. end
  232. journal.start( )
  233. end
  234. self.disconnect = function ( )
  235. for u, t in pairs( users ) do
  236. data[ u ].lifetime = data[ u ].lifetime + ( journal.optime - data[ u ].newlogin )
  237. end
  238. db_commit( )
  239. journal.reset( )
  240. data = nil
  241. users = nil
  242. end
  243. self.create_record = function ( username, password )
  244. -- don't allow clobbering existing users
  245. if data[ username ] then return false end
  246. local rec =
  247. {
  248. password = password,
  249. oldlogin = -1,
  250. newlogin = -1,
  251. lifetime = 0,
  252. total_sessions = 0,
  253. total_attempts = 0,
  254. total_failures = 0,
  255. approved_addrs = { },
  256. assigned_privs = { },
  257. }
  258. data[ username ] = rec
  259. journal.record_raw( TX_CREATE, username, password )
  260. return true
  261. end
  262. self.delete_record = function ( username )
  263. -- don't allow deletion of online users or non-existent users
  264. if not data[ username ] or users[ username ] then return false end
  265. data[ username ] = nil
  266. journal.record_raw( TX_DELETE, username )
  267. return true
  268. end
  269. self.set_password = function ( username, password )
  270. if not data[ username ] then return false end
  271. data[ username ].password = password
  272. journal.record_raw( TX_SET_PASSWORD, username, password )
  273. return true
  274. end
  275. self.set_assigned_privs = function ( username, assigned_privs )
  276. if not data[ username ] then return false end
  277. data[ username ].assigned_privs = assigned_privs
  278. journal.record_raw( TX_SET_ASSIGNED_PRIVS, username, table.concat( assigned_privs, "," ) )
  279. return true
  280. end
  281. self.set_approved_addrs = function ( username, approved_addrs )
  282. if not data[ username ] then return false end
  283. data[ username ].approved_addrs = approved_addrs
  284. journal.record_raw( TX_SET_APPROVED_ADDRS, username, table.concat( approved_addrs, "," ) )
  285. return true
  286. end
  287. self.on_session_opened = function ( username )
  288. data[ username ].total_sessions = data[ username ].total_sessions + 1
  289. journal.record_raw( TX_SESSION_OPENED, username )
  290. end
  291. self.on_session_closed = function ( username )
  292. data[ username ].lifetime = data[ username ].lifetime + ( journal.optime - data[ username ].newlogin )
  293. users[ username ] = nil
  294. journal.record_raw( TX_SESSION_CLOSED, username )
  295. end
  296. self.on_login_attempt = function ( username, ip )
  297. data[ username ].total_attempts = data[ username ].total_attempts + 1
  298. journal.record_raw( TX_LOGIN_ATTEMPT, username, ip )
  299. end
  300. self.on_login_failure = function ( username, ip )
  301. data[ username ].total_failures = data[ username ].total_failures + 1
  302. journal.record_raw( TX_LOGIN_FAILURE, username, ip )
  303. end
  304. self.on_login_success = function ( username, ip )
  305. if data[ username ].oldlogin == -1 then
  306. data[ username ].oldlogin = journal.optime
  307. end
  308. users[ username ] = data[ username ].newlogin
  309. data[ username ].newlogin = journal.optime
  310. journal.record_raw( TX_LOGIN_SUCCESS, username, ip )
  311. end
  312. self.records = function ( )
  313. return pairs( data )
  314. end
  315. self.records_match = function ( pattern )
  316. local k
  317. return function ( )
  318. local v
  319. local p = string.lower( pattern )
  320. while true do
  321. k, v = next( data, k )
  322. if not k then
  323. return
  324. elseif string.match( string.lower( k ), p ) then
  325. return k, v
  326. end
  327. end
  328. end
  329. end
  330. self.select_record = function ( username )
  331. return data[ username ]
  332. end
  333. self.search = function ( is_online, pattern )
  334. local res = { }
  335. local src = is_online and users or data
  336. for k, v in pairs( src ) do
  337. if pattern == nil or string.match( k, pattern ) then
  338. table.insert( res, k )
  339. end
  340. end
  341. return res
  342. end
  343. return self
  344. end