Mod lua custom auth
From D3xt3r01.tk
WHY
Because BasicAuth seemed a bad way to do stuff. With multiple .htaccess files all over the place... I wanted to be able to expire users. To mail them when they're not active .. etc. You need to have at least 2.4.7
HOW
My /etc/apache2/lua_scripts/authcheck_hook.lua
require 'apache2'
JSON = require "JSON"
processhostnames = { '192.168.1.95', 'd3xbucharest.go.ro', 'd3xt3r01.tk' }
function ivmsqlarrayset(r)
local db, err = r:dbacquire("mod_dbd")
if not db then
r:info("ivmsqlarrayset(): [500] DB Error: " .. err)
return 500
end
local cdirgroup = {}
local dirs, err = db:select(r, "SELECT * FROM `groupdirs` ORDER BY LENGTH(`dir`) DESC")
if not err then
local rows = dirs(0)
for k, row in pairs(rows) do
if row[3] == "" then
row[3] = " "
end
table.insert(cdirgroup, row[2] .. ":" .. row[3] .. ":" .. row[4])
end
else
r:info("ivmsqlarrayset(): Could not connect to the database: " .. err)
end
r:ivm_set("cdirgroup", JSON:encode(cdirgroup))
db:close()
return nil
end
function explode(d,p)
local t, ll
t={}
ll=0
if(#p == 1) then return {p} end
while true do
l=string.find(p,d,ll,true)
if l ~= nil then
table.insert(t, string.sub(p,ll,l-1))
ll=l+1
else
table.insert(t, string.sub(p,ll))
break
end
end
return t
end
function ugallow(r, usergroup, cdirgroup)
r:info("ugallow(): usergroup: " .. usergroup)
for key,value in pairs(cdirgroup) do
local dir, group, skip = value:match("^([^:]+):([^:]+):([^:]+)$")
if r.uri:match("^" .. dir .. "?") then
dirgroups = group
r:info("ugallow(): dirgroups: '" .. group .. "'")
break
end
end
local authpass = nil
if dirgroups ~= " " then
local usergroups = explode(" ", usergroup)
dirgroups = explode(" ", dirgroups)
if usergroups then
for k, row in pairs(dirgroups) do
for j, row2 in pairs(usergroups) do
if row == row2 then
authpass = true
end
end
end
else
authpass = true
end
else
authpass = true
end
if authpass then
r:info("ugallow(): return true")
return true
else
r:info("ugallow(): return nil")
return nil
end
end
function authcheck_hook(r)
if r.uri == "/favicon.ico" then return apache2.DECLINED end
for key,value in pairs(processhostnames) do
if r.hostname == value then
process = true
end
end
if not process then
return apache2.DECLINED
end
r:info("authcheck_hook(): ----------------")
local okay = false
r:info("authcheck_hook(): running auth for " .. r.uri)
local cdir = r:ivm_get("cdirgroup")
if not cdir then
ivmsqlarrayset(r)
end
local expcdir = r:ivm_get("expcdir")
if not expcdir then
r:ivm_set("expcdir", os.time())
expcdir = os.time()
else
local cdirexpcheck = os.time() - expcdir
if cdirexpcheck > 60 then
ivmsqlarrayset(r)
r:ivm_set("expcdir", os.time())
end
end
local cdirgroup = JSON:decode(r:ivm_get("cdirgroup"))
local directives = "no"
for key,value in pairs(cdirgroup) do
local dir, group, skip = value:match("^([^:]+):([^:]+):([^:]+)$")
if r.uri:match("^" .. dir .. "?") then
if skip == "true" then
r:info("authcheck_hook(): apache2.DECLINED because skip : " .. dir)
return apache2.DECLINED
end
r:info("authcheck_hook(): Matched a policy .. will process...")
directives = "yes"
dirgroups = group
break
end
end
if directives == "no" then return apache2.DECLINED end
r:info("authcheck_hook(): PROCESSING REQUEST !!!")
local db, err = r:dbacquire("mod_dbd")
if not db then
r:info("authcheck_hook(): [500] DB Error: " .. err)
return 500
end
local str = (r.headers_in['Authorization'] or ""):match("^Basic (.+)$")
local decoded = r:base64_decode(str or "")
local usr, pass = decoded:match("^([^:]+):([^:]+)$")
r:info(("authcheck_hook(): Checking against user %s and password %s ..."):format(usr or "nil", pass or "nil"))
if usr == nil or pass == nil then
r:info("authcheck_hook(): user or pass is nil, returning 401")
r.err_headers_out['WWW-Authenticate'] = 'Basic realm="Restricted Files"'
db:close()
return 401
else
pass = r:sha1(pass)
r:info("authcheck_hook(): User & password not nil .. checking cache")
local cached_and_okay = false
local cached_entry = r:ivm_get("auth_cache:" .. usr)
if cached_entry then
local expiry, password, usergroup = cached_entry:match("^(%d+):([^:]+):([^:]+)$")
expiry = tonumber(expiry)
local expcheck = os.time() - expiry
r:info("authcheck_hook(): " .. usr .. " found in cache .. " .. expcheck .. " expiry in ".. (300 - expcheck))
if expcheck < 300 then
cached_and_okay = true
r:info("authcheck_hook(): Comparing '" .. password .. "' with '" .. pass .. "'")
local authpass = ugallow(r, usergroup, cdirgroup)
if not authpass then
r:info("authcheck_hook(): Group match failed... throwing: 401")
db:close()
r.err_headers_out['WWW-Authenticate'] = 'Basic realm="Restricted Files"'
return 401
end
if password == pass then
r:info("authcheck_hook(): apache2.OK")
r.user = usr
db:close()
return apache2.OK
end
end
end
if not cached_and_okay then
r:info("authcheck_hook(): " .. usr .. " not cached .. searching ..")
local prep = db:prepare(r, "SELECT `id`, `expiry`, `groups` FROM `auth` WHERE `user` = %s AND `password` = %s AND `enabled` = '1' AND (`expiry` = '0000-00-00 00:00:00' OR `expiry` > NOW()) LIMIT 1")
local result = prep:select(usr, pass)
if result then
r:info("authcheck_hook(): Did the query")
local rows = result(0)
if #rows == 1 then
r:info("authcheck_hook(): We got results...")
okay = true
local row = rows[1]
local id = row[1]
local expiry = row[2]
usergroup = row[3]
r:info("authcheck_hook(): One row ... id: " .. id .. " expiry: " .. expiry)
if expiry ~= '0000-00-00 00:00:00' then
local prep = db:prepare(r, "UPDATE `auth` SET `expiry` = NOW()+INTERVAL 1 MONTH, `last_accessed` = NOW(), `mailed` = '0' WHERE `id` = %s LIMIT 1")
prep:query(id)
end
local authpass = ugallow(r, usergroup, cdirgroup)
if not authpass then
r:info("authcheck_hook(): Group match failed... throwing: 401")
db:close()
r.err_headers_out['WWW-Authenticate'] = 'Basic realm="Restricted Files"'
return 401
end
else
r:info("authcheck_hook(): Received sha1pass: " .. pass)
r:info("authcheck_hook(): Wrong username and/or password ... throwing: 401")
db:close()
r.err_headers_out['WWW-Authenticate'] = 'Basic realm="Restricted Files"'
return 401
end
r:ivm_set("auth_cache:" .. usr, os.time() .. ":" .. pass .. ":" .. usergroup)
else
r:info("authcheck_hook(): No results .. 401 again")
r.err_headers_out['WWW-Authenticate'] = 'Basic realm="Restricted Files"'
db:close()
return 401
end
end
end
if not okay then
r:info("authcheck_hook(): Wrong username or password .. throwing: 401")
r.err_headers_out['WWW-Authenticate'] = 'Basic realm="Restricted Files"'
db:close()
return 401
end
r:info("authcheck_hook(): apache2.OK")
r.user = usr
db:close()
return apache2.OK
end
Place the JSON.lua in the same directory.
My mod_lua.conf
DBDParams host=localhost,dbname=apache,user=apacheupdate,pass=P4ssw0rd
DBDriver mysql
DBDMax 20
LoadModule lua_module modules/mod_lua.so
AddHandler lua-script .lua
#LogLevel mod_lua.c:info
LuaScope thread
LuaCodeCache stat
LuaRoot /etc/apache2/lua_scripts/
LuaHookAccessChecker authcheck_hook.lua authcheck_hook
CREATE TABLE IF NOT EXISTS `auth` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user` varchar(32) NOT NULL,
`password` varchar(64) NOT NULL,
`email` varchar(64) NOT NULL,
`groups` varchar(128) NOT NULL,
`enabled` tinyint(1) DEFAULT '1',
`expiry` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_accessed` timestamp NULL DEFAULT NULL,
`mailed` tinyint(1) NOT NULL DEFAULT '0',
`comment` tinytext NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=66 ;
CREATE TABLE IF NOT EXISTS `groupdirs` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`dir` varchar(128) NOT NULL,
`groups` varchar(128) NOT NULL,
`skip` enum('true','false') NOT NULL DEFAULT 'false',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=9 ;