mirror of
https://github.com/YunoHost/SSOwat.git
synced 2024-09-03 20:06:27 +02:00
248 lines
7.1 KiB
Lua
248 lines
7.1 KiB
Lua
--
|
|
-- helpers.lua
|
|
--
|
|
-- This is a file called at every request by the `access.lua` file. It contains
|
|
-- a set of useful functions related to HTTP and LDAP.
|
|
--
|
|
|
|
module('helpers', package.seeall)
|
|
|
|
local cache = ngx.shared.cache
|
|
local conf = config.get_config()
|
|
local Logging = require("logging")
|
|
local jwt = require("vendor.luajwtjitsi.luajwtjitsi")
|
|
local cipher = require('openssl.cipher')
|
|
local mime = require("mime")
|
|
|
|
local appender = function(self, level, message)
|
|
|
|
-- Output to log file
|
|
local fp = io.open(log_file, "a")
|
|
local str = string.format("[%-6s%s] %s\n", level:upper(), os.date(), message)
|
|
fp:write(str)
|
|
fp:close()
|
|
|
|
return true
|
|
end
|
|
|
|
local logger = Logging.new(appender)
|
|
--logger:setLevel(logger.DEBUG) -- FIXME
|
|
|
|
|
|
-- Import Perl regular expressions library
|
|
local rex = require "rex_pcre"
|
|
|
|
local is_logged_in = false
|
|
|
|
function refresh_config()
|
|
conf = config.get_config()
|
|
end
|
|
|
|
function get_config()
|
|
return conf
|
|
end
|
|
|
|
-- The 'match' function uses PCRE regex as default
|
|
-- If '%.' is found in the regex, we assume it's a LUA regex (legacy code)
|
|
-- 'match' returns the matched text.
|
|
function match(s, regex)
|
|
if not string.find(regex, '%%%.') then
|
|
return rex.match(s, regex)
|
|
else
|
|
return string.match(s,regex)
|
|
end
|
|
end
|
|
|
|
-- Read a FS stored file
|
|
function read_file(file)
|
|
local f = io.open(file, "rb")
|
|
if not f then return false end
|
|
local content = f:read("*all")
|
|
f:close()
|
|
return content
|
|
end
|
|
|
|
|
|
-- Lua has no sugar :D
|
|
function is_in_table(t, v)
|
|
for key, value in ipairs(t) do
|
|
if value == v then return key end
|
|
end
|
|
end
|
|
|
|
|
|
-- Get the index of a value in a table
|
|
function index_of(t,val)
|
|
for k,v in ipairs(t) do
|
|
if v == val then return k end
|
|
end
|
|
end
|
|
|
|
|
|
-- Test whether a string starts with another
|
|
function string.starts(String, Start)
|
|
if not String then
|
|
return false
|
|
end
|
|
return string.sub(String, 1, string.len(Start)) == Start
|
|
end
|
|
|
|
|
|
-- Test whether a string ends with another
|
|
function string.ends(String, End)
|
|
return End=='' or string.sub(String, -string.len(End)) == End
|
|
end
|
|
|
|
|
|
-- Convert a table of arguments to an URI string
|
|
function uri_args_string(args)
|
|
if not args then
|
|
args = ngx.req.get_uri_args()
|
|
end
|
|
String = "?"
|
|
for k,v in pairs(args) do
|
|
String = String..tostring(k).."="..tostring(v).."&"
|
|
end
|
|
return string.sub(String, 1, string.len(String) - 1)
|
|
end
|
|
|
|
|
|
-- Validate authentification
|
|
--
|
|
-- Check if the session cookies are set, and rehash server + client information
|
|
-- to match the session hash.
|
|
--
|
|
function check_authentication()
|
|
|
|
local token = ngx.var["cookie_" .. conf["cookie_name"]]
|
|
|
|
decoded, err = jwt.verify(token, "HS256", cookie_secret)
|
|
|
|
if err ~= nil then
|
|
-- FIXME : log an authentication error to be caught by fail2ban ? or should it happen somewhere else ? (check the old code)
|
|
authUser = nil
|
|
authPasswordEnc = nil
|
|
is_logged_in = false
|
|
return is_logged_in
|
|
end
|
|
|
|
-- cf. src/authenticators/ldap_ynhuser.py in YunoHost to see how the cookie is actually created
|
|
authUser = decoded["user"]
|
|
authPasswordEnc = decoded["pwd"]
|
|
is_logged_in = true
|
|
|
|
-- Gotta update authUser and is_logged_in
|
|
return is_logged_in
|
|
end
|
|
|
|
-- Extract the user password from cookie,
|
|
-- needed to create the basic auth header
|
|
function decrypt_user_password()
|
|
-- authPasswordEnc is actually a string formatted as <password_enc_b64>|<iv_b64>
|
|
-- For example: ctl8kk5GevYdaA5VZ2S88Q==|yTAzCx0Gd1+MCit4EQl9lA==
|
|
-- The password is encoded using AES-256-CBC with the IV being the right-side data
|
|
local password_enc_b64, iv_b64 = authPasswordEnc:match("([^|]+)|([^|]+)")
|
|
local password_enc = mime.unb64(password_enc_b64)
|
|
local iv = mime.unb64(iv_b64)
|
|
return cipher.new('aes-256-cbc'):decrypt(cookie_secret, iv):final(password_enc)
|
|
end
|
|
|
|
-- Check whether a user is allowed to access a URL using the `permissions` directive
|
|
-- of the configuration file
|
|
function has_access(permission, user)
|
|
user = user or authUser
|
|
|
|
if permission == nil then
|
|
logger:debug("No permission matching request for "..ngx.var.uri)
|
|
return false
|
|
end
|
|
|
|
-- Public access
|
|
if user == nil or permission["public"] then
|
|
user = user or "A visitor"
|
|
logger:debug(user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
|
return permission["public"]
|
|
end
|
|
|
|
logger:debug("User "..user.." tries to access "..ngx.var.uri.." (corresponding perm: "..permission["id"]..")")
|
|
|
|
-- The user has permission to access the content if he is in the list of allowed users
|
|
if element_is_in_table(user, permission["users"]) then
|
|
logger:debug("User "..user.." can access "..ngx.var.host..ngx.var.uri..uri_args_string())
|
|
return true
|
|
else
|
|
logger:debug("User "..user.." cannot access "..ngx.var.uri)
|
|
return false
|
|
end
|
|
end
|
|
|
|
function element_is_in_table(element, table)
|
|
if table then
|
|
for _, el in pairs(table) do
|
|
if el == element then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Set the authentication headers in order to pass credentials to the
|
|
-- application underneath.
|
|
function set_basic_auth_header(user)
|
|
local user = user or authUser
|
|
-- Set `Authorization` header to enable HTTP authentification
|
|
ngx.req.set_header("Authorization", "Basic "..ngx.encode_base64(
|
|
user..":"..decrypt_user_password()
|
|
))
|
|
end
|
|
|
|
|
|
-- Set cookie and redirect (needed to properly set cookie)
|
|
function redirect(url)
|
|
logger:debug("Redirecting to "..url)
|
|
-- For security reason we don't allow to redirect onto unknown domain
|
|
-- And if `uri_args.r` contains line break, someone is probably trying to
|
|
-- pass some additional headers
|
|
|
|
-- This should cover the following cases:
|
|
-- https://malicious.domain.tld/foo/bar
|
|
-- http://malicious.domain.tld/foo/bar
|
|
-- https://malicious.domain.tld:1234/foo
|
|
-- malicious.domain.tld/foo/bar
|
|
-- (/foo/bar, in which case no need to make sure it's prefixed with https://)
|
|
if not string.starts(url, "/") and not string.starts(url, "http://") and not string.starts(url, "https://") then
|
|
url = "https://"..url
|
|
end
|
|
local is_known_domain = string.starts(url, "/")
|
|
for _, domain in ipairs(conf["domains"]) do
|
|
if is_known_domain then
|
|
break
|
|
end
|
|
-- Replace - character to %- because - is a special char for regex in lua
|
|
domain = string.gsub(domain, "%-","%%-")
|
|
is_known_domain = is_known_domain or url:match("^https?://"..domain.."/?") ~= nil
|
|
end
|
|
if string.match(url, "(.*)\n") or not is_known_domain then
|
|
logger:debug("Unauthorized redirection to "..url)
|
|
url = conf.portal_url
|
|
end
|
|
return ngx.redirect(url)
|
|
end
|
|
|
|
|
|
-- Set cookie and go on with the response (needed to properly set cookie)
|
|
function pass()
|
|
logger:debug("Allowing to pass through "..ngx.var.uri)
|
|
|
|
-- When we are in the SSOwat portal, we need a default `content-type`
|
|
if string.ends(ngx.var.uri, "/")
|
|
or string.ends(ngx.var.uri, ".html")
|
|
or string.ends(ngx.var.uri, ".htm")
|
|
then
|
|
ngx.header["Content-Type"] = "text/html"
|
|
end
|
|
|
|
return
|
|
end
|