Mod lua custom auth

From D3xt3r01.tk
Jump to navigationJump to search

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 ;