Browse Source

Build 06

- 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
master
Leslie Krause 1 year ago
parent
commit
5b5ccd7b51
7 changed files with 744 additions and 384 deletions
  1. 12
    1
      README.txt
  2. 402
    0
      db.lua
  3. 3
    383
      init.lua
  4. 0
    0
      tools/convert.awk
  5. 223
    0
      tools/extract.awk
  6. 38
    0
      tools/revert.awk
  7. 66
    0
      tools/rollback.lua

+ 12
- 1
README.txt View File

@@ -1,4 +1,4 @@
1
-Auth Redux Mod v2.3b
1
+Auth Redux Mod v2.4b
2 2
 By Leslie Krause
3 3
 
4 4
 Auth Redux is a drop-in replacement for the builtin authentication handler of Minetest.
@@ -43,6 +43,17 @@ Version 2.3b (08-Jul-2018)
43 43
   - changed database search method to use Lua regexes
44 44
   - removed hard-coded file names from database methods
45 45
 
46
+Version 2.4b (13-Jul-2018)
47
+  - moved Journal and AuthDatabase classes into library
48
+  - added rollback function to AuthDatabase class
49
+  - reworked journal audit to support rollback option
50
+  - better encapsulated database commit function
51
+  - allowed for STOPPED opcode during database update
52
+  - various changes to error and action messages
53
+  - moved command-line scripts to separate directory
54
+  - included script to rollback database via journal
55
+  - included script to extract debug log into journal
56
+
46 57
 Installation
47 58
 ----------------------
48 59
 

+ 402
- 0
db.lua View File

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

+ 3
- 383
init.lua View File

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

convert.awk → tools/convert.awk View File


+ 223
- 0
tools/extract.awk View File

@@ -0,0 +1,223 @@
1
+#!/bin/awk -f
2
+
3
+################################################################################
4
+# Database Import Script for Auth Redux Mod (Step 2)
5
+# ---------------------------------------------------
6
+# WARNING: This script is to be run immediately after the database conversion.
7
+#
8
+# This script will extract player login activity from the specified 'debug.txt'
9
+# file and produce an import journal in the world directory. For best results,
10
+# the 'auth.txt' and 'debug.txt' files should be complete and current. If they 
11
+# are not entirely synchronous, then errors are likely to result.
12
+#
13
+# Lastly, run the rollback.lua script from the command line. This will replay
14
+# the import journal against the newly converted database, applying all player
15
+# login activity as it was derived from the corresponding debug log.
16
+#
17
+# Below is the required sequence of commands:
18
+#
19
+# > awk -f convert.awk -v mode=convert ~/.minetest/worlds/world/auth.txt
20
+# > awk -f extract.awk ~/.minetest/worlds/world/auth.txt ~/.minetest/debug.txt
21
+# > lua rollback.lua ~/.minetest/worlds/world/auth.db
22
+#
23
+# It is not necessary to operate on the original files. They can be moved into
24
+# into a temporary subdirectory for use with all of these scripts.
25
+#
26
+# WARNING: The 'auth.dbx' file will be overwritten. This should not cause any
27
+# problems if you began with a freshly converted database. When in doubt, then
28
+# you may want to backup the 'auth.db' and 'auth.dbx' files as a precaution.
29
+#
30
+# For more detailed output, you can change the debug level as follows:
31
+#
32
+# > awk -f extract.awk -v debug=verbose ...
33
+#
34
+# This will display both errors and warnings. It might be helpful to redirect 
35
+# output to a temporary file for this purpose. To see only errors, the default
36
+# debug level of 'terse' should be sufficient.
37
+#
38
+# Warnings occur if there are orphaned accounts in the 'auth.txt' file that do 
39
+# not appear in the corresponding debug log. This happens when the debug log
40
+# is incomplete. Orphaned accounts will have no player login activity applied.
41
+#
42
+# Errors occur if the 'auth.txt' file is inconsistent with the debug log. This
43
+# could be the result of a server crash or intentional deletion of accounts.
44
+# Such accounts are deemed invalid and player login activity will be ignored.
45
+# However, you should verify that you are using the correct 'auth.txt' file.
46
+################################################################################
47
+
48
+# USAGE EXAMPLE:
49
+# awk -f extract.awk -v debug=terse /home/minetest/.minetest/worlds/world/auth.txt /home/minetest/.minetest/debug.txt
50
+
51
+function get_timestamp( date_str, time_str ) {
52
+	return mktime( sprintf( "%d %d %d %d %d %d", \
53
+		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 ) ) );
54
+}
55
+
56
+function print_info( source, result ) {
57
+	if( is_verbose == 1 ) {
58
+		print "[" source "]", result;
59
+	}
60
+}
61
+
62
+function check_user( name ) {
63
+	if( name in db_users ) {
64
+		++db_users[ name ];
65
+		return 0;
66
+	}
67
+
68
+	if( !log_users[ name ] ) {
69
+		print "ERROR: Player '" name "' does not exist in auth.txt file.";
70
+	}
71
+	return ++log_users[ name ];
72
+}
73
+
74
+function trim( str ) {
75
+	return substr( str, 2, length( str ) - 2 )
76
+}
77
+
78
+BEGIN {
79
+	FS = " "
80
+
81
+	LOG_STARTED = 10
82
+	LOG_STOPPED = 12
83
+	LOG_TOUCHED = 13
84
+	TX_SESSION_OPENED = 50
85
+	TX_SESSION_CLOSED = 51
86
+	TX_LOGIN_FAILURE = 31
87
+	TX_LOGIN_SUCCESS = 32
88
+
89
+	journal_file = "auth.dbx";
90
+	is_started = 0;
91
+	is_verbose = 0;
92
+
93
+	total_records = 0;
94
+
95
+	if( debug == "verbose" ) {
96
+		is_verbose = 1;
97
+	}
98
+	else if( debug != "terse" ) {
99
+                print "ERROR: Unknown argument, defaulting to terse debug level.";
100
+	}
101
+
102
+	if( ARGC != 3 ) {
103
+		print( "The required arguments are missing, aborting." )
104
+		exit 1;
105
+	}
106
+
107
+	world_path = ARGV[ 1 ];
108
+	if( sub( /auth.txt$/, "", world_path ) == 0 ) {
109
+		print( "The specified auth.txt file is not recognized, aborting." )
110
+		exit 1;
111
+	}
112
+
113
+	if( ARGV[ 2 ] != "debug.txt" && ARGV[ 2 ] !~ /\/debug.txt$/ ) {
114
+		print( "The specified debug.txt file is not recognized, aborting." )
115
+		exit 1;
116
+	}
117
+}
118
+
119
+# parse the 'auth.txt' file
120
+
121
+ARGIND == 1 && FNR == 1 {
122
+	print( "Reading the " world_path "auth.txt file..." )
123
+}
124
+
125
+ARGIND == 1 {
126
+	name = substr( $0, 1, index( $0, ":" ) - 1 )
127
+	db_users[ name ] = 0;
128
+	total_records++;
129
+}
130
+
131
+# parse the 'debug.txt' file
132
+
133
+ARGIND == 2 && FNR == 1 {
134
+	print( "Reading the " world_path "debug.txt file..." )
135
+}
136
+
137
+ARGIND == 2 {
138
+	cur_date_str = $1;
139
+	cur_time_str = $2;
140
+
141
+	if( $3 == "ACTION[Main]:" && ( $4 FS $5 ) == "World at" ) {
142
+
143
+		# 2018-07-10 12:16:09: ACTION[Main]: World at [/root/.minetest/worlds/world]
144
+		print_info( "debug.txt", $1 " @connect" );
145
+
146
+		print get_timestamp( cur_date_str, cur_time_str ), LOG_STARTED > ( world_path journal_file );
147
+		is_started = 1;
148
+	}
149
+	else if( !is_started ) {
150
+		# sanity check since mod loading errors precede startup logging
151
+		# and shutdowns are not always immediate after they are logged.
152
+		next;
153
+	}
154
+	else if( $3 == "[Main]:" && $5 == "sigint_handler():" || $3 == "ERROR[Main]:" && ( $4 FS $5 ) == "stack traceback:" ) {
155
+
156
+		# 2018-07-10 06:47:46: [Main]: INFO: sigint_handler(): Ctrl-C pressed, shutting down.
157
+		# 2018-07-10 16:18:52: ERROR[Main]: stack traceback:
158
+		print_info( "debug.txt", $1 " @disconnect " ( $4 == "stack" ? "(err)" : "(sig)" ) );
159
+
160
+		print get_timestamp( cur_date_str, cur_time_str ), LOG_STOPPED > ( world_path journal_file );
161
+		is_started = 0;
162
+	}
163
+	else if( $3 == "ACTION[Server]:" && ( $5 == "joins" || $6 == "joins" || $5 == "leaves" || $5 == "times" || $5 == "shuts" || $4 == "Server:" ) ) {	# optimization hack
164
+		cur_action_str = substr( $0, 38 );
165
+
166
+		if( cur_action_str ~ /^[a-zA-Z0-9_-]+ \[[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\] joins game\./ ) {
167
+
168
+			# 2017-06-09 16:49:26: ACTION[Server]: sorcerykid [127.0.0.1] joins game.
169
+#			print_info( "debug.txt", $1 " @on_login_success " $4 FS trim( $5 ) );
170
+
171
+			if( check_user( $4 ) > 0 ) next;
172
+			print get_timestamp( cur_date_str, cur_time_str ), TX_LOGIN_SUCCESS, $4, trim( $5 ) > ( world_path journal_file );
173
+		}
174
+		else if( cur_action_str ~ /^Server: User [a-zA-Z0-9_-]+ at [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ supplied wrong password/ ) {
175
+
176
+			# 2017-06-09 20:35:20: ACTION[Server]: Server: User sorcerykid at 127.0.0.1 supplied wrong password (auth mechanism: SRP)
177
+#			print_info( "debug.txt", $1 " @on_login_failure " $6 FS $8 );
178
+
179
+			if( check_user( $6 ) > 0 ) next;
180
+			print get_timestamp( cur_date_str, cur_time_str ), TX_LOGIN_FAILURE, $6, $8 > ( world_path journal_file );
181
+		}
182
+		else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ joins game\./ ) {
183
+
184
+			# 2017-06-09 16:49:26: ACTION[Server]: sorcerykid joins game. List of players: 
185
+#			print_info( "debug.txt", $1 " @on_session_opened " $4 );
186
+
187
+			if( check_user( $4 ) > 0 ) next;
188
+			print get_timestamp( cur_date_str, cur_time_str ), TX_SESSION_OPENED, $4 > ( world_path journal_file );
189
+		}
190
+		else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ leaves game\./ || cur_action_str ~ /[a-zA-Z0-9_-]+ times out\./ ) {
191
+
192
+			# 2017-06-09 20:32:32: ACTION[Server]: sorcerykid leaves game. List of players: 
193
+			# 2017-06-09 20:34:47: ACTION[Server]: sorcerykid times out. List of players: 
194
+#			print_info( "debug.txt", $1 " @on_session_closed " $4 );
195
+
196
+			if( check_user( $4 ) > 0 ) next;
197
+			print get_timestamp( cur_date_str, cur_time_str ), TX_SESSION_CLOSED, $4 > ( world_path journal_file );
198
+		}
199
+		else if( cur_action_str ~ /^[a-zA-Z0-9_-]+ shuts down server/ ) {
200
+
201
+			# 2017-06-09 20:32:32: ACTION[Server]: sorcerykid shuts down server
202
+#			print_info( "debug.txt", $1 " @disconnect (req)" );
203
+
204
+			print get_timestamp( cur_date_str, cur_time_str ), LOG_STOPPED > ( world_path journal_file );
205
+			is_started = 0;
206
+		}
207
+
208
+	}
209
+}
210
+
211
+END {
212
+	total_orphans = 0;
213
+	for( name in db_users ) {
214
+		if( db_users[ name ] == 0 ) {
215
+			if( is_verbose == 1 ) {
216
+				print "WARNING: No player activity for '" name "' in debug.txt file.";
217
+			}
218
+			total_orphans++;
219
+		}
220
+	}
221
+	print "Total accounts in database: " total_records " (" total_orphans " orphaned accounts)";
222
+	print "Done!"
223
+}

+ 38
- 0
tools/revert.awk View File

@@ -0,0 +1,38 @@
1
+#!/bin/awk -f
2
+
3
+################################################################################
4
+# Database Export Script for Auth Redux Mod
5
+# ------------------------------------------
6
+# This script will revert to the default 'auth.txt' flat-file database required
7
+# by the builtin authentication handler.
8
+#
9
+# EXAMPLE:
10
+# awk -f revert.awk ~/.minetest/worlds/world/auth.db
11
+################################################################################
12
+
13
+BEGIN {
14
+	FS = ":";
15
+	OFS = ":";
16
+	db_file = "auth.txt";
17
+
18
+	path = ARGV[ 1 ]
19
+	if( sub( /[-_A-Za-z0-9]+\.db$/, "", path ) == 0 ) {
20
+		# sanity check for nonstandard input file
21
+		path = "";
22
+	}
23
+
24
+	print "Reverting " ARGV[ 1 ] "...";
25
+}
26
+
27
+NF == 10 {
28
+	username = $1;
29
+	password = $2;
30
+	assigned_privs = $10;
31
+	newlogin = $4;
32
+
33
+	print username, password, assigned_privs, newlogin > path db_file;
34
+}
35
+
36
+END {
37
+	print "Done!"
38
+}

+ 66
- 0
tools/rollback.lua View File

@@ -0,0 +1,66 @@
1
+--------------------------------------------------------
2
+-- Minetest :: Auth Redux Mod v2.4 (auth_rx)
3
+--
4
+-- See README.txt for licensing and release notes.
5
+-- Copyright (c) 2017-2018, Leslie E. Krause
6
+--------------------------------------------------------
7
+
8
+minetest = { }
9
+
10
+function string.split( str, sep, has_nil )
11
+	res = { }
12
+	for val in string.gmatch( str .. sep, "(.-)" .. sep ) do
13
+		if val ~= "" or has_nil then
14
+			table.insert( res, val )
15
+		end
16
+	end
17
+	return res
18
+end
19
+
20
+minetest.log = function ( act, str )
21
+	print( "[" .. act .. "]", str )
22
+end
23
+
24
+minetest.register_globalstep = function ( ) end
25
+
26
+--------------------------------------------------------
27
+
28
+dofile( "../db.lua" )
29
+
30
+local name = "auth.db"
31
+local path = "."
32
+
33
+print( "******************************************************" )
34
+print( "* This script will rollback the Auth Redux database. *" )
35
+print( "* Do not proceed unless you know what you are doing! *" )
36
+print( "* -------------------------------------------------- *" )
37
+print( "* Usage Example:                                     *" )
38
+print( "* lua rollback.lua ~/.minetest/worlds/world/auth.db  *" )
39
+print( "******************************************************" )
40
+
41
+if arg[ 1 ] and arg[ 1 ] ~= "auth.db" then
42
+	path = string.match( arg[ 1 ], "^(.*)/auth%.db$" )
43
+	if not path then
44
+		error( "Invalid arguments specified." )
45
+	end
46
+end
47
+
48
+print( "The following database will be modified:" )
49
+print( "  " .. path .. "/" .. name )
50
+print( )
51
+
52
+io.write( "Do you wish to continue (y/n)? " )
53
+local opt = io.read( 1 )
54
+
55
+if opt == "y" then
56
+	print( "Initiating rollback procedure..." )
57
+
58
+	local auth_db = AuthDatabase( path, name )
59
+	auth_db.rollback( )
60
+
61
+	os.rename( path .. "/" .. name .. "x", path .. "/~" .. name .. "x" )
62
+
63
+	if not io.open( path .. "/" .. name .. "x", "w+b" ) then
64
+		minetest.log( "error", "Cannot open " .. path .. "/~" .. name .. " for writing." )
65
+	end
66
+end

Loading…
Cancel
Save