Mod lua custom auth

From D3xt3r01.tk
Jump to navigationJump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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 ;