aboutsummaryrefslogtreecommitdiffstats
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/datamanager.lua30
-rw-r--r--util/dependencies.lua158
-rw-r--r--util/events.lua8
-rw-r--r--util/hmac.lua49
-rw-r--r--util/pluginloader.lua15
-rw-r--r--util/prosodyctl.lua4
-rw-r--r--util/sasl.lua317
-rw-r--r--util/sasl/anonymous.lua36
-rw-r--r--util/sasl/digest-md5.lua243
-rw-r--r--util/sasl/plain.lua82
-rw-r--r--util/sasl/scram.lua222
-rw-r--r--util/sasl_cyrus.lua176
-rw-r--r--util/stanza.lua36
-rw-r--r--util/timer.lua70
14 files changed, 1062 insertions, 384 deletions
diff --git a/util/datamanager.lua b/util/datamanager.lua
index 6058cbc6..57cd2594 100644
--- a/util/datamanager.lua
+++ b/util/datamanager.lua
@@ -21,13 +21,20 @@ local next = next;
local t_insert = table.insert;
local append = require "util.serialization".append;
local path_separator = "/"; if os.getenv("WINDIR") then path_separator = "\\" end
-local lfs_mkdir = require "lfs".mkdir;
+local lfs = require "lfs";
+local raw_mkdir;
+
+if prosody.platform == "posix" then
+ raw_mkdir = require "util.pposix".mkdir; -- Doesn't trample on umask
+else
+ raw_mkdir = lfs.mkdir;
+end
module "datamanager"
---- utils -----
local encode, decode;
-do
+do
local urlcodes = setmetatable({}, { __index = function (t, k) t[k] = char(tonumber("0x"..k)); return t[k]; end });
decode = function (s)
@@ -43,7 +50,7 @@ local _mkdir = {};
local function mkdir(path)
path = path:gsub("/", path_separator); -- TODO as an optimization, do this during path creation rather than here
if not _mkdir[path] then
- lfs_mkdir(path);
+ raw_mkdir(path);
_mkdir[path] = true;
end
return path;
@@ -88,7 +95,7 @@ end
function getpath(username, host, datastore, ext, create)
ext = ext or "dat";
- host = host and encode(host);
+ host = (host and encode(host)) or "_global";
username = username and encode(username);
if username then
if create then mkdir(mkdir(mkdir(data_path).."/"..host).."/"..datastore); end
@@ -105,14 +112,21 @@ end
function load(username, host, datastore)
local data, ret = loadfile(getpath(username, host, datastore));
if not data then
- log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
- return nil;
+ local mode = lfs.attributes(getpath(username, host, datastore), "mode");
+ if not mode then
+ log("debug", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
+ return nil;
+ else -- file exists, but can't be read
+ -- TODO more detailed error checking and logging?
+ log("error", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
+ return nil, "Error reading storage";
+ end
end
setfenv(data, {});
local success, ret = pcall(data);
if not success then
log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
- return nil;
+ return nil, "Error reading storage";
end
return ret;
end
@@ -131,7 +145,7 @@ function store(username, host, datastore, data)
local f, msg = io_open(getpath(username, host, datastore, nil, true), "w+");
if not f then
log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil"));
- return;
+ return nil, "Error saving to storage";
end
f:write("return ");
append(f, data);
diff --git a/util/dependencies.lua b/util/dependencies.lua
index 1c8dc375..6024dd63 100644
--- a/util/dependencies.lua
+++ b/util/dependencies.lua
@@ -6,12 +6,16 @@
-- COPYING file in the source package for more information.
--
+module("dependencies", package.seeall)
-local fatal;
+function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end
-local function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end
+-- Required to be able to find packages installed with luarocks
+if not softreq "luarocks.loader" then -- LuaRocks 2.x
+ softreq "luarocks.require"; -- LuaRocks <1.x
+end
-local function missingdep(name, sources, msg)
+function missingdep(name, sources, msg)
print("");
print("**************************");
print("Prosody was unable to find "..tostring(name));
@@ -31,89 +35,91 @@ local function missingdep(name, sources, msg)
print("");
end
-local lxp = softreq "lxp"
-
-if not lxp then
- missingdep("luaexpat", {
- ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-expat0";
- ["luarocks"] = "luarocks install luaexpat";
- ["Source"] = "http://www.keplerproject.org/luaexpat/";
- });
- fatal = true;
-end
-
-local socket = softreq "socket"
-
-if not socket then
- missingdep("luasocket", {
- ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-socket2";
- ["luarocks"] = "luarocks install luasocket";
- ["Source"] = "http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/";
- });
- fatal = true;
-end
+function check_dependencies()
+ local fatal;
-local lfs, err = softreq "lfs"
-if not lfs then
- missingdep("luafilesystem", {
- ["luarocks"] = "luarocks install luafilesystem";
- ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-filesystem0";
- ["Source"] = "http://www.keplerproject.org/luafilesystem/";
- });
- fatal = true;
-end
-
-local ssl = softreq "ssl"
-
-if not ssl then
- if config.get("*", "core", "run_without_ssl") then
- log("warn", "Running without SSL support because run_without_ssl is defined in the config");
- else
+ local lxp = softreq "lxp"
+
+ if not lxp then
+ missingdep("luaexpat", {
+ ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-expat0";
+ ["luarocks"] = "luarocks install luaexpat";
+ ["Source"] = "http://www.keplerproject.org/luaexpat/";
+ });
+ fatal = true;
+ end
+
+ local socket = softreq "socket"
+
+ if not socket then
+ missingdep("luasocket", {
+ ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-socket2";
+ ["luarocks"] = "luarocks install luasocket";
+ ["Source"] = "http://www.tecgraf.puc-rio.br/~diego/professional/luasocket/";
+ });
+ fatal = true;
+ end
+
+ local lfs, err = softreq "lfs"
+ if not lfs then
+ missingdep("luafilesystem", {
+ ["luarocks"] = "luarocks install luafilesystem";
+ ["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-filesystem0";
+ ["Source"] = "http://www.keplerproject.org/luafilesystem/";
+ });
+ fatal = true;
+ end
+
+ local ssl = softreq "ssl"
+
+ if not ssl then
missingdep("LuaSec", {
["Debian/Ubuntu"] = "http://prosody.im/download/start#debian_and_ubuntu";
["luarocks"] = "luarocks install luasec";
["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/";
}, "SSL/TLS support will not be available");
+ else
+ local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)");
+ if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then
+ log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends");
+ end
end
-else
- local major, minor, veryminor, patched = ssl._VERSION:match("(%d+)%.(%d+)%.?(%d*)(M?)");
- if not major or ((tonumber(major) == 0 and (tonumber(minor) or 0) <= 3 and (tonumber(veryminor) or 0) <= 2) and patched ~= "M") then
- log("error", "This version of LuaSec contains a known bug that causes disconnects, see http://prosody.im/doc/depends");
+
+ local encodings, err = softreq "util.encodings"
+ if not encodings then
+ if err:match("not found") then
+ missingdep("util.encodings", { ["Windows"] = "Make sure you have encodings.dll from the Prosody distribution in util/";
+ ["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/encodings.so";
+ });
+ else
+ print "***********************************"
+ print("util/encodings couldn't be loaded. Check that you have a recent version of libidn");
+ print ""
+ print("The full error was:");
+ print(err)
+ print "***********************************"
+ end
+ fatal = true;
end
-end
-local encodings, err = softreq "util.encodings"
-if not encodings then
- if err:match("not found") then
- missingdep("util.encodings", { ["Windows"] = "Make sure you have encodings.dll from the Prosody distribution in util/";
- ["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/encodings.so";
- });
- else
- print "***********************************"
- print("util/encodings couldn't be loaded. Check that you have a recent version of libidn");
- print ""
- print("The full error was:");
- print(err)
- print "***********************************"
+ local hashes, err = softreq "util.hashes"
+ if not hashes then
+ if err:match("not found") then
+ missingdep("util.hashes", { ["Windows"] = "Make sure you have hashes.dll from the Prosody distribution in util/";
+ ["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/hashes.so";
+ });
+ else
+ print "***********************************"
+ print("util/hashes couldn't be loaded. Check that you have a recent version of OpenSSL (libcrypto in particular)");
+ print ""
+ print("The full error was:");
+ print(err)
+ print "***********************************"
+ end
+ fatal = true;
end
- fatal = true;
+ return not fatal;
end
-local hashes, err = softreq "util.hashes"
-if not hashes then
- if err:match("not found") then
- missingdep("util.hashes", { ["Windows"] = "Make sure you have hashes.dll from the Prosody distribution in util/";
- ["GNU/Linux"] = "Run './configure' and 'make' in the Prosody source directory to build util/hashes.so";
- });
- else
- print "***********************************"
- print("util/hashes couldn't be loaded. Check that you have a recent version of OpenSSL (libcrypto in particular)");
- print ""
- print("The full error was:");
- print(err)
- print "***********************************"
- end
- fatal = true;
-end
-if fatal then os.exit(1); end
+return _M;
diff --git a/util/events.lua b/util/events.lua
index 68954a56..363d2ac6 100644
--- a/util/events.lua
+++ b/util/events.lua
@@ -47,13 +47,13 @@ function new()
_rebuild_index(event);
end
end;
- local function add_plugin(plugin)
- for event, handler in pairs(plugin) do
+ local function add_handlers(handlers)
+ for event, handler in pairs(handlers) do
add_handler(event, handler);
end
end;
- local function remove_plugin(plugin)
- for event, handler in pairs(plugin) do
+ local function remove_handlers(handlers)
+ for event, handler in pairs(handlers) do
remove_handler(event, handler);
end
end;
diff --git a/util/hmac.lua b/util/hmac.lua
index 26da1d78..66dd41d8 100644
--- a/util/hmac.lua
+++ b/util/hmac.lua
@@ -7,20 +7,27 @@
--
local hashes = require "util.hashes"
-local xor = require "bit".bxor
-local t_insert, t_concat = table.insert, table.concat;
local s_char = string.char;
+local s_gsub = string.gsub;
+local s_rep = string.rep;
module "hmac"
-local function arraystr(array)
- local t = {}
- for i = 1,#array do
- t_insert(t, s_char(array[i]))
- end
-
- return t_concat(t)
+local xor_map = {0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;1;0;3;2;5;4;7;6;9;8;11;10;13;12;15;14;2;3;0;1;6;7;4;5;10;11;8;9;14;15;12;13;3;2;1;0;7;6;5;4;11;10;9;8;15;14;13;12;4;5;6;7;0;1;2;3;12;13;14;15;8;9;10;11;5;4;7;6;1;0;3;2;13;12;15;14;9;8;11;10;6;7;4;5;2;3;0;1;14;15;12;13;10;11;8;9;7;6;5;4;3;2;1;0;15;14;13;12;11;10;9;8;8;9;10;11;12;13;14;15;0;1;2;3;4;5;6;7;9;8;11;10;13;12;15;14;1;0;3;2;5;4;7;6;10;11;8;9;14;15;12;13;2;3;0;1;6;7;4;5;11;10;9;8;15;14;13;12;3;2;1;0;7;6;5;4;12;13;14;15;8;9;10;11;4;5;6;7;0;1;2;3;13;12;15;14;9;8;11;10;5;4;7;6;1;0;3;2;14;15;12;13;10;11;8;9;6;7;4;5;2;3;0;1;15;14;13;12;11;10;9;8;7;6;5;4;3;2;1;0;};
+local function xor(x, y)
+ local lowx, lowy = x % 16, y % 16;
+ local hix, hiy = (x - lowx) / 16, (y - lowy) / 16;
+ local lowr, hir = xor_map[lowx * 16 + lowy + 1], xor_map[hix * 16 + hiy + 1];
+ local r = hir * 16 + lowr;
+ return r;
+end
+local opadc, ipadc = s_char(0x5c), s_char(0x36);
+local ipad_map = {};
+local opad_map = {};
+for i=0,255 do
+ ipad_map[s_char(i)] = s_char(xor(0x36, i));
+ opad_map[s_char(i)] = s_char(xor(0x5c, i));
end
--[[
@@ -36,31 +43,15 @@ hex
return raw hash or hexadecimal string
--]]
function hmac(key, message, hash, blocksize, hex)
- local opad = {}
- local ipad = {}
-
- for i = 1,blocksize do
- opad[i] = 0x5c
- ipad[i] = 0x36
- end
-
if #key > blocksize then
key = hash(key)
end
- for i = 1,#key do
- ipad[i] = xor(ipad[i],key:sub(i,i):byte())
- opad[i] = xor(opad[i],key:sub(i,i):byte())
- end
-
- opad = arraystr(opad)
- ipad = arraystr(ipad)
+ local padding = blocksize - #key;
+ local ipad = s_gsub(key, ".", ipad_map)..s_rep(ipadc, padding);
+ local opad = s_gsub(key, ".", opad_map)..s_rep(opadc, padding);
- if hex then
- return hash(opad..hash(ipad..message), true)
- else
- return hash(opad..hash(ipad..message))
- end
+ return hash(opad..hash(ipad..message), hex)
end
function md5(key, message, hex)
diff --git a/util/pluginloader.lua b/util/pluginloader.lua
index aedec4b4..956b92bd 100644
--- a/util/pluginloader.lua
+++ b/util/pluginloader.lua
@@ -9,8 +9,10 @@
local plugin_dir = CFG_PLUGINDIR or "./plugins/";
-local io_open = io.open;
-local loadstring = loadstring;
+local io_open, os_time = io.open, os.time;
+local loadstring, pairs = loadstring, pairs;
+
+local datamanager = require "util.datamanager";
module "pluginloader"
@@ -22,13 +24,16 @@ local function load_file(name)
return content, name;
end
-function load_resource(plugin, resource)
+function load_resource(plugin, resource, loader)
if not resource then
resource = "mod_"..plugin..".lua";
end
- local content, err = load_file(plugin.."/"..resource);
- if not content then content, err = load_file(resource); end
+ loader = loader or load_file;
+
+ local content, err = loader(plugin.."/"..resource);
+ if not content then content, err = loader(resource); end
-- TODO add support for packed plugins
+
return content, err;
end
diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua
index 8131fed9..04d58d1d 100644
--- a/util/prosodyctl.lua
+++ b/util/prosodyctl.lua
@@ -109,10 +109,8 @@ function start()
end
if not CFG_SOURCEDIR then
os.execute("./prosody");
- elseif CFG_SOURCEDIR:match("^/usr/local") then
- os.execute("/usr/local/bin/prosody");
else
- os.execute("prosody");
+ os.execute(CFG_SOURCEDIR.."/../../bin/prosody");
end
return true;
end
diff --git a/util/sasl.lua b/util/sasl.lua
index f65e7062..306acc0c 100644
--- a/util/sasl.lua
+++ b/util/sasl.lua
@@ -14,258 +14,119 @@
local md5 = require "util.hashes".md5;
local log = require "util.logger".init("sasl");
-local tostring = tostring;
local st = require "util.stanza";
-local generate_uuid = require "util.uuid".generate;
-local t_insert, t_concat = table.insert, table.concat;
-local to_byte, to_char = string.byte, string.char;
+local set = require "util.set";
+local array = require "util.array";
local to_unicode = require "util.encodings".idna.to_unicode;
+
+local tostring = tostring;
+local pairs, ipairs = pairs, ipairs;
+local t_insert, t_concat = table.insert, table.concat;
local s_match = string.match;
-local gmatch = string.gmatch
-local string = string
-local math = require "math"
local type = type
local error = error
-local print = print
-
-module "sasl"
+local setmetatable = setmetatable;
+local assert = assert;
+local require = require;
--- Credentials handler:
--- Arguments: ("PLAIN", user, host, password)
--- Returns: true (success) | false (fail) | nil (user unknown)
-local function new_plain(realm, credentials_handler)
- local object = { mechanism = "PLAIN", realm = realm, credentials_handler = credentials_handler}
- function object.feed(self, message)
- if message == "" or message == nil then return "failure", "malformed-request" end
- local response = message
- local authorization = s_match(response, "([^%z]*)")
- local authentication = s_match(response, "%z([^%z]+)%z")
- local password = s_match(response, "%z[^%z]+%z([^%z]+)")
+require "util.iterators"
+local keys = keys
- if authentication == nil or password == nil then return "failure", "malformed-request" end
- self.username = authentication
- local auth_success = self.credentials_handler("PLAIN", self.username, self.realm, password)
+local array = require "util.array"
+module "sasl"
- if auth_success then
- return "success"
- elseif auth_success == nil then
- return "failure", "account-disabled"
- else
- return "failure", "not-authorized"
- end
- end
- return object
+--[[
+Authentication Backend Prototypes:
+
+state = false : disabled
+state = true : enabled
+state = nil : non-existant
+]]
+
+local method = {};
+method.__index = method;
+local mechanisms = {};
+local backend_mechanism = {};
+
+-- register a new SASL mechanims
+local function registerMechanism(name, backends, f)
+ assert(type(name) == "string", "Parameter name MUST be a string.");
+ assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table.");
+ assert(type(f) == "function", "Parameter f MUST be a function.");
+ mechanisms[name] = f
+ for _, backend_name in ipairs(backends) do
+ if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end
+ t_insert(backend_mechanism[backend_name], name);
+ end
end
--- credentials_handler:
--- Arguments: (mechanism, node, domain, realm, decoder)
--- Returns: Password encoding, (plaintext) password
--- implementing RFC 2831
-local function new_digest_md5(realm, credentials_handler)
- --TODO complete support for authzid
-
- local function serialize(message)
- local data = ""
+-- create a new SASL object which can be used to authenticate clients
+function new(realm, profile, forbidden)
+ local sasl_i = {profile = profile};
+ sasl_i.realm = realm;
+ local s = setmetatable(sasl_i, method);
+ if forbidden == nil then forbidden = {} end
+ s:forbidden(forbidden)
+ return s;
+end
- if type(message) ~= "table" then error("serialize needs an argument of type table.") end
+-- get a fresh clone with the same realm, profiles and forbidden mechanisms
+function method:clean_clone()
+ return new(self.realm, self.profile, self:forbidden())
+end
- -- testing all possible values
- if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
- if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
- if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
- if message["charset"] then data = data..[[charset=]]..message.charset.."," end
- if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
- if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
- data = data:gsub(",$", "")
- return data
+-- set the forbidden mechanisms
+function method:forbidden( restrict )
+ if restrict then
+ -- set forbidden
+ self.restrict = set.new(restrict);
+ else
+ -- get forbidden
+ return array.collect(self.restrict:items());
end
+end
- local function utf8tolatin1ifpossible(passwd)
- local i = 1;
- while i <= #passwd do
- local passwd_i = to_byte(passwd:sub(i, i));
- if passwd_i > 0x7F then
- if passwd_i < 0xC0 or passwd_i > 0xC3 then
- return passwd;
+-- get a list of possible SASL mechanims to use
+function method:mechanisms()
+ local mechanisms = {}
+ for backend, f in pairs(self.profile) do
+ if backend_mechanism[backend] then
+ for _, mechanism in ipairs(backend_mechanism[backend]) do
+ if not self.restrict:contains(mechanism) then
+ mechanisms[mechanism] = true;
end
- i = i + 1;
- passwd_i = to_byte(passwd:sub(i, i));
- if passwd_i < 0x80 or passwd_i > 0xBF then
- return passwd;
- end
- end
- i = i + 1;
- end
-
- local p = {};
- local j = 0;
- i = 1;
- while (i <= #passwd) do
- local passwd_i = to_byte(passwd:sub(i, i));
- if passwd_i > 0x7F then
- i = i + 1;
- local passwd_i_1 = to_byte(passwd:sub(i, i));
- t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
- else
- t_insert(p, to_char(passwd_i));
- end
- i = i + 1;
- end
- return t_concat(p);
- end
- local function latin1toutf8(str)
- local p = {};
- for ch in gmatch(str, ".") do
- ch = to_byte(ch);
- if (ch < 0x80) then
- t_insert(p, to_char(ch));
- elseif (ch < 0xC0) then
- t_insert(p, to_char(0xC2, ch));
- else
- t_insert(p, to_char(0xC3, ch - 64));
end
end
- return t_concat(p);
end
- local function parse(data)
- local message = {}
- -- COMPAT: %z in the pattern to work around jwchat bug (sends "charset=utf-8\0")
- for k, v in gmatch(data, [[([%w%-]+)="?([^",%z]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
- message[k] = v;
- end
- return message;
- end
-
- local object = { mechanism = "DIGEST-MD5", realm = realm, credentials_handler = credentials_handler};
-
- object.nonce = generate_uuid();
- object.step = 0;
- object.nonce_count = {};
-
- function object.feed(self, message)
- self.step = self.step + 1;
- if (self.step == 1) then
- local challenge = serialize({ nonce = object.nonce,
- qop = "auth",
- charset = "utf-8",
- algorithm = "md5-sess",
- realm = self.realm});
- return "challenge", challenge;
- elseif (self.step == 2) then
- local response = parse(message);
- -- check for replay attack
- if response["nc"] then
- if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
- end
-
- -- check for username, it's REQUIRED by RFC 2831
- if not response["username"] then
- return "failure", "malformed-request";
- end
- self["username"] = response["username"];
-
- -- check for nonce, ...
- if not response["nonce"] then
- return "failure", "malformed-request";
- else
- -- check if it's the right nonce
- if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
- end
-
- if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
- if not response["qop"] then response["qop"] = "auth" end
-
- if response["realm"] == nil or response["realm"] == "" then
- response["realm"] = "";
- elseif response["realm"] ~= self.realm then
- return "failure", "not-authorized", "Incorrect realm value";
- end
-
- local decoder;
- if response["charset"] == nil then
- decoder = utf8tolatin1ifpossible;
- elseif response["charset"] ~= "utf-8" then
- return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8.";
- end
-
- local domain = "";
- local protocol = "";
- if response["digest-uri"] then
- protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
- if protocol == nil or domain == nil then return "failure", "malformed-request" end
- else
- return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
- end
-
- --TODO maybe realm support
- self.username = response["username"];
- local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder);
- if Y == nil then return "failure", "not-authorized"
- elseif Y == false then return "failure", "account-disabled" end
- local A1 = "";
- if response.authzid then
- if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then
- -- COMPAT
- log("warn", "Client is violating RFC 3920 (section 6.1, point 7).");
- A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
- else
- return "failure", "invalid-authzid";
- end
- else
- A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
- end
- local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
-
- local HA1 = md5(A1, true);
- local HA2 = md5(A2, true);
-
- local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
- local response_value = md5(KD, true);
-
- if response_value == response["response"] then
- -- calculate rspauth
- A2 = ":"..protocol.."/"..domain;
-
- HA1 = md5(A1, true);
- HA2 = md5(A2, true);
+ self["possible_mechanisms"] = mechanisms;
+ return array.collect(keys(mechanisms));
+end
- KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
- local rspauth = md5(KD, true);
- self.authenticated = true;
- return "challenge", serialize({rspauth = rspauth});
- else
- return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
- end
- elseif self.step == 3 then
- if self.authenticated ~= nil then return "success"
- else return "failure", "malformed-request" end
- end
+-- select a mechanism to use
+function method:select(mechanism)
+ if self.mech_i then
+ return false;
+ end
+
+ self.mech_i = mechanisms[mechanism]
+ if self.mech_i == nil then
+ return false;
end
- return object;
+ return true;
end
--- Credentials handler: Can be nil. If specified, should take the mechanism as
--- the only argument, and return true for OK, or false for not-OK (TODO)
-local function new_anonymous(realm, credentials_handler)
- local object = { mechanism = "ANONYMOUS", realm = realm, credentials_handler = credentials_handler}
- function object.feed(self, message)
- return "success"
- end
- object["username"] = generate_uuid()
- return object
+-- feed new messages to process into the library
+function method:process(message)
+ --if message == "" or message == nil then return "failure", "malformed-request" end
+ return self.mech_i(self, message);
end
-
-function new(mechanism, realm, credentials_handler)
- local object
- if mechanism == "PLAIN" then object = new_plain(realm, credentials_handler)
- elseif mechanism == "DIGEST-MD5" then object = new_digest_md5(realm, credentials_handler)
- elseif mechanism == "ANONYMOUS" then object = new_anonymous(realm, credentials_handler)
- else
- log("debug", "Unsupported SASL mechanism: "..tostring(mechanism));
- return nil
- end
- return object
+-- load the mechanisms
+local load_mechs = {"plain", "digest-md5", "anonymous", "scram"}
+for _, mech in ipairs(load_mechs) do
+ local name = "util.sasl."..mech;
+ local m = require(name);
+ m.init(registerMechanism)
end
return _M;
diff --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua
new file mode 100644
index 00000000..7b5a5081
--- /dev/null
+++ b/util/sasl/anonymous.lua
@@ -0,0 +1,36 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2010 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local s_match = string.match;
+
+local log = require "util.logger".init("sasl");
+local generate_uuid = require "util.uuid".generate;
+
+module "anonymous"
+
+--=========================
+--SASL ANONYMOUS according to RFC 4505
+local function anonymous(self, message)
+ local username;
+ repeat
+ username = generate_uuid();
+ until self.profile.anonymous(username, self.realm);
+ self["username"] = username;
+ return "success"
+end
+
+function init(registerMechanism)
+ registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
+end
+
+return _M; \ No newline at end of file
diff --git a/util/sasl/digest-md5.lua b/util/sasl/digest-md5.lua
new file mode 100644
index 00000000..2837148e
--- /dev/null
+++ b/util/sasl/digest-md5.lua
@@ -0,0 +1,243 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2010 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local tostring = tostring;
+local type = type;
+
+local s_gmatch = string.gmatch;
+local s_match = string.match;
+local t_concat = table.concat;
+local t_insert = table.insert;
+local to_byte, to_char = string.byte, string.char;
+
+local md5 = require "util.hashes".md5;
+local log = require "util.logger".init("sasl");
+local generate_uuid = require "util.uuid".generate;
+
+module "digest-md5"
+
+--=========================
+--SASL DIGEST-MD5 according to RFC 2831
+
+--[[
+Supported Authentication Backends
+
+digest_md5:
+ function(username, domain, realm, encoding) -- domain and realm are usually the same; for some broken
+ -- implementations it's not
+ return digesthash, state;
+ end
+
+digest_md5_test:
+ function(username, domain, realm, encoding, digesthash)
+ return true or false, state;
+ end
+]]
+
+local function digest(self, message)
+ --TODO complete support for authzid
+
+ local function serialize(message)
+ local data = ""
+
+ -- testing all possible values
+ if message["realm"] then data = data..[[realm="]]..message.realm..[[",]] end
+ if message["nonce"] then data = data..[[nonce="]]..message.nonce..[[",]] end
+ if message["qop"] then data = data..[[qop="]]..message.qop..[[",]] end
+ if message["charset"] then data = data..[[charset=]]..message.charset.."," end
+ if message["algorithm"] then data = data..[[algorithm=]]..message.algorithm.."," end
+ if message["rspauth"] then data = data..[[rspauth=]]..message.rspauth.."," end
+ data = data:gsub(",$", "")
+ return data
+ end
+
+ local function utf8tolatin1ifpossible(passwd)
+ local i = 1;
+ while i <= #passwd do
+ local passwd_i = to_byte(passwd:sub(i, i));
+ if passwd_i > 0x7F then
+ if passwd_i < 0xC0 or passwd_i > 0xC3 then
+ return passwd;
+ end
+ i = i + 1;
+ passwd_i = to_byte(passwd:sub(i, i));
+ if passwd_i < 0x80 or passwd_i > 0xBF then
+ return passwd;
+ end
+ end
+ i = i + 1;
+ end
+
+ local p = {};
+ local j = 0;
+ i = 1;
+ while (i <= #passwd) do
+ local passwd_i = to_byte(passwd:sub(i, i));
+ if passwd_i > 0x7F then
+ i = i + 1;
+ local passwd_i_1 = to_byte(passwd:sub(i, i));
+ t_insert(p, to_char(passwd_i%4*64 + passwd_i_1%64)); -- I'm so clever
+ else
+ t_insert(p, to_char(passwd_i));
+ end
+ i = i + 1;
+ end
+ return t_concat(p);
+ end
+ local function latin1toutf8(str)
+ local p = {};
+ for ch in s_gmatch(str, ".") do
+ ch = to_byte(ch);
+ if (ch < 0x80) then
+ t_insert(p, to_char(ch));
+ elseif (ch < 0xC0) then
+ t_insert(p, to_char(0xC2, ch));
+ else
+ t_insert(p, to_char(0xC3, ch - 64));
+ end
+ end
+ return t_concat(p);
+ end
+ local function parse(data)
+ local message = {}
+ -- COMPAT: %z in the pattern to work around jwchat bug (sends "charset=utf-8\0")
+ for k, v in s_gmatch(data, [[([%w%-]+)="?([^",%z]*)"?,?]]) do -- FIXME The hacky regex makes me shudder
+ message[k] = v;
+ end
+ return message;
+ end
+
+ if not self.nonce then
+ self.nonce = generate_uuid();
+ self.step = 0;
+ self.nonce_count = {};
+ end
+
+ self.step = self.step + 1;
+ if (self.step == 1) then
+ local challenge = serialize({ nonce = self.nonce,
+ qop = "auth",
+ charset = "utf-8",
+ algorithm = "md5-sess",
+ realm = self.realm});
+ return "challenge", challenge;
+ elseif (self.step == 2) then
+ local response = parse(message);
+ -- check for replay attack
+ if response["nc"] then
+ if self.nonce_count[response["nc"]] then return "failure", "not-authorized" end
+ end
+
+ -- check for username, it's REQUIRED by RFC 2831
+ if not response["username"] then
+ return "failure", "malformed-request";
+ end
+ self["username"] = response["username"];
+
+ -- check for nonce, ...
+ if not response["nonce"] then
+ return "failure", "malformed-request";
+ else
+ -- check if it's the right nonce
+ if response["nonce"] ~= tostring(self.nonce) then return "failure", "malformed-request" end
+ end
+
+ if not response["cnonce"] then return "failure", "malformed-request", "Missing entry for cnonce in SASL message." end
+ if not response["qop"] then response["qop"] = "auth" end
+
+ if response["realm"] == nil or response["realm"] == "" then
+ response["realm"] = "";
+ elseif response["realm"] ~= self.realm then
+ return "failure", "not-authorized", "Incorrect realm value";
+ end
+
+ local decoder;
+ if response["charset"] == nil then
+ decoder = utf8tolatin1ifpossible;
+ elseif response["charset"] ~= "utf-8" then
+ return "failure", "incorrect-encoding", "The client's response uses "..response["charset"].." for encoding with isn't supported by sasl.lua. Supported encodings are latin or utf-8.";
+ end
+
+ local domain = "";
+ local protocol = "";
+ if response["digest-uri"] then
+ protocol, domain = response["digest-uri"]:match("(%w+)/(.*)$");
+ if protocol == nil or domain == nil then return "failure", "malformed-request" end
+ else
+ return "failure", "malformed-request", "Missing entry for digest-uri in SASL message."
+ end
+
+ --TODO maybe realm support
+ self.username = response["username"];
+ local Y, state;
+ if self.profile.plain then
+ local password, state = self.profile.plain(response["username"], self.realm)
+ if state == nil then return "failure", "not-authorized"
+ elseif state == false then return "failure", "account-disabled" end
+ Y = md5(response["username"]..":"..response["realm"]..":"..password);
+ elseif self.profile["digest-md5"] then
+ Y, state = self.profile["digest-md5"](response["username"], self.realm, response["realm"], response["charset"])
+ if state == nil then return "failure", "not-authorized"
+ elseif state == false then return "failure", "account-disabled" end
+ elseif self.profile["digest-md5-test"] then
+ -- TODO
+ end
+ --local password_encoding, Y = self.credentials_handler("DIGEST-MD5", response["username"], self.realm, response["realm"], decoder);
+ --if Y == nil then return "failure", "not-authorized"
+ --elseif Y == false then return "failure", "account-disabled" end
+ local A1 = "";
+ if response.authzid then
+ if response.authzid == self.username or response.authzid == self.username.."@"..self.realm then
+ -- COMPAT
+ log("warn", "Client is violating RFC 3920 (section 6.1, point 7).");
+ A1 = Y..":"..response["nonce"]..":"..response["cnonce"]..":"..response.authzid;
+ else
+ return "failure", "invalid-authzid";
+ end
+ else
+ A1 = Y..":"..response["nonce"]..":"..response["cnonce"];
+ end
+ local A2 = "AUTHENTICATE:"..protocol.."/"..domain;
+
+ local HA1 = md5(A1, true);
+ local HA2 = md5(A2, true);
+
+ local KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2;
+ local response_value = md5(KD, true);
+
+ if response_value == response["response"] then
+ -- calculate rspauth
+ A2 = ":"..protocol.."/"..domain;
+
+ HA1 = md5(A1, true);
+ HA2 = md5(A2, true);
+
+ KD = HA1..":"..response["nonce"]..":"..response["nc"]..":"..response["cnonce"]..":"..response["qop"]..":"..HA2
+ local rspauth = md5(KD, true);
+ self.authenticated = true;
+ --TODO: considering sending the rspauth in a success node for saving one roundtrip; allowed according to http://tools.ietf.org/html/draft-saintandre-rfc3920bis-09#section-7.3.6
+ return "challenge", serialize({rspauth = rspauth});
+ else
+ return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated."
+ end
+ elseif self.step == 3 then
+ if self.authenticated ~= nil then return "success"
+ else return "failure", "malformed-request" end
+ end
+end
+
+function init(registerMechanism)
+ registerMechanism("DIGEST-MD5", {"plain"}, digest);
+end
+
+return _M; \ No newline at end of file
diff --git a/util/sasl/plain.lua b/util/sasl/plain.lua
new file mode 100644
index 00000000..39821182
--- /dev/null
+++ b/util/sasl/plain.lua
@@ -0,0 +1,82 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2010 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local s_match = string.match;
+local saslprep = require "util.encodings".stringprep.saslprep;
+local log = require "util.logger".init("sasl");
+
+module "plain"
+
+-- ================================
+-- SASL PLAIN according to RFC 4616
+
+--[[
+Supported Authentication Backends
+
+plain:
+ function(username, realm)
+ return password, state;
+ end
+
+plain_test:
+ function(username, realm, password)
+ return true or false, state;
+ end
+]]
+
+local function plain(self, message)
+ if not message then
+ return "failure", "malformed-request";
+ end
+
+ local authorization, authentication, password = s_match(message, "^([^%z]*)%z([^%z]+)%z([^%z]+)");
+
+ if not authorization then
+ return "failure", "malformed-request";
+ end
+
+ -- SASLprep password and authentication
+ authentication = saslprep(authentication);
+ password = saslprep(password);
+
+ if (not password) or (password == "") or (not authentication) or (authentication == "") then
+ log("debug", "Username or password violates SASLprep.");
+ return "failure", "malformed-request", "Invalid username or password.";
+ end
+
+ local correct, state = false, false;
+ if self.profile.plain then
+ local correct_password;
+ correct_password, state = self.profile.plain(authentication, self.realm);
+ if correct_password == password then correct = true; else correct = false; end
+ elseif self.profile.plain_test then
+ correct, state = self.profile.plain_test(authentication, self.realm, password);
+ end
+
+ self.username = authentication
+ if not state then
+ return "failure", "account-disabled";
+ end
+
+ if correct then
+ return "success";
+ else
+ return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent.";
+ end
+end
+
+function init(registerMechanism)
+ registerMechanism("PLAIN", {"plain", "plain_test"}, plain);
+end
+
+return _M;
diff --git a/util/sasl/scram.lua b/util/sasl/scram.lua
new file mode 100644
index 00000000..1340423c
--- /dev/null
+++ b/util/sasl/scram.lua
@@ -0,0 +1,222 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2010 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local s_match = string.match;
+local type = type
+local string = string
+local base64 = require "util.encodings".base64;
+local hmac_sha1 = require "util.hmac".sha1;
+local sha1 = require "util.hashes".sha1;
+local generate_uuid = require "util.uuid".generate;
+local saslprep = require "util.encodings".stringprep.saslprep;
+local log = require "util.logger".init("sasl");
+local t_concat = table.concat;
+local char = string.char;
+local byte = string.byte;
+
+module "scram"
+
+--=========================
+--SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10
+
+--[[
+Supported Authentication Backends
+
+scram_{MECH}:
+ -- MECH being a standard hash name (like those at IANA's hash registry) with '-' replaced with '_'
+ function(username, realm)
+ return salted_password, iteration_count, salt, state;
+ end
+]]
+
+local default_i = 4096
+
+local function bp( b )
+ local result = ""
+ for i=1, b:len() do
+ result = result.."\\"..b:byte(i)
+ end
+ return result
+end
+
+local xor_map = {0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;1;0;3;2;5;4;7;6;9;8;11;10;13;12;15;14;2;3;0;1;6;7;4;5;10;11;8;9;14;15;12;13;3;2;1;0;7;6;5;4;11;10;9;8;15;14;13;12;4;5;6;7;0;1;2;3;12;13;14;15;8;9;10;11;5;4;7;6;1;0;3;2;13;12;15;14;9;8;11;10;6;7;4;5;2;3;0;1;14;15;12;13;10;11;8;9;7;6;5;4;3;2;1;0;15;14;13;12;11;10;9;8;8;9;10;11;12;13;14;15;0;1;2;3;4;5;6;7;9;8;11;10;13;12;15;14;1;0;3;2;5;4;7;6;10;11;8;9;14;15;12;13;2;3;0;1;6;7;4;5;11;10;9;8;15;14;13;12;3;2;1;0;7;6;5;4;12;13;14;15;8;9;10;11;4;5;6;7;0;1;2;3;13;12;15;14;9;8;11;10;5;4;7;6;1;0;3;2;14;15;12;13;10;11;8;9;6;7;4;5;2;3;0;1;15;14;13;12;11;10;9;8;7;6;5;4;3;2;1;0;};
+
+local result = {};
+local function binaryXOR( a, b )
+ for i=1, #a do
+ local x, y = byte(a, i), byte(b, i);
+ local lowx, lowy = x % 16, y % 16;
+ local hix, hiy = (x - lowx) / 16, (y - lowy) / 16;
+ local lowr, hir = xor_map[lowx * 16 + lowy + 1], xor_map[hix * 16 + hiy + 1];
+ local r = hir * 16 + lowr;
+ result[i] = char(r)
+ end
+ return t_concat(result);
+end
+
+-- hash algorithm independent Hi(PBKDF2) implementation
+local function Hi(hmac, str, salt, i)
+ local Ust = hmac(str, salt.."\0\0\0\1");
+ local res = Ust;
+ for n=1,i-1 do
+ local Und = hmac(str, Ust)
+ res = binaryXOR(res, Und)
+ Ust = Und
+ end
+ return res
+end
+
+local function validate_username(username)
+ -- check for forbidden char sequences
+ for eq in username:gmatch("=(.?.?)") do
+ if eq ~= "2D" and eq ~= "3D" then
+ return false
+ end
+ end
+
+ -- replace =2D with , and =3D with =
+ username = username:gsub("=2D", ",");
+ username = username:gsub("=3D", "=");
+
+ -- apply SASLprep
+ username = saslprep(username);
+ return username;
+end
+
+local function hashprep( hashname )
+ local hash = hashname:lower()
+ hash = hash:gsub("-", "_")
+ return hash
+end
+
+function saltedPasswordSHA1(password, salt, iteration_count)
+ local salted_password
+ if type(password) ~= "string" or type(salt) ~= "string" or type(iteration_count) ~= "number" then
+ return false, "inappropriate argument types"
+ end
+ if iteration_count < 4096 then
+ log("warn", "Iteration count < 4096 which is the suggested minimum according to RFC 5802.")
+ end
+
+ return true, Hi(hmac_sha1, password, salt, iteration_count);
+end
+
+local function scram_gen(hash_name, H_f, HMAC_f)
+ local function scram_hash(self, message)
+ if not self.state then self["state"] = {} end
+
+ if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end
+ if not self.state.name then
+ -- we are processing client_first_message
+ local client_first_message = message;
+
+ -- TODO: fail if authzid is provided, since we don't support them yet
+ self.state["client_first_message"] = client_first_message;
+ self.state["gs2_cbind_flag"], self.state["authzid"], self.state["name"], self.state["clientnonce"]
+ = client_first_message:match("^(%a),(.*),n=(.*),r=([^,]*).*");
+
+ -- we don't do any channel binding yet
+ if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" then
+ return "failure", "malformed-request";
+ end
+
+ if not self.state.name or not self.state.clientnonce then
+ return "failure", "malformed-request", "Channel binding isn't support at this time.";
+ end
+
+ self.state.name = validate_username(self.state.name);
+ if not self.state.name then
+ log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
+ return "failure", "malformed-request", "Invalid username.";
+ end
+
+ self.state["servernonce"] = generate_uuid();
+
+ -- retreive credentials
+ if self.profile.plain then
+ local password, state = self.profile.plain(self.state.name, self.realm)
+ if state == nil then return "failure", "not-authorized"
+ elseif state == false then return "failure", "account-disabled" end
+
+ password = saslprep(password);
+ if not password then
+ log("debug", "Password violates SASLprep.");
+ return "failure", "not-authorized", "Invalid password."
+ end
+
+ self.state.salt = generate_uuid();
+ self.state.iteration_count = default_i;
+
+ local succ = false;
+ succ, self.state.salted_password = saltedPasswordSHA1(password, self.state.salt, default_i, self.state.iteration_count);
+ if not succ then
+ log("error", "Generating salted password failed. Reason: %s", self.state.salted_password);
+ return "failure", "temporary-auth-failure";
+ end
+ elseif self.profile["scram_"..hashprep(hash_name)] then
+ local salted_password, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self.state.name, self.realm);
+ if state == nil then return "failure", "not-authorized"
+ elseif state == false then return "failure", "account-disabled" end
+
+ self.state.salted_password = salted_password;
+ self.state.iteration_count = iteration_count;
+ self.state.salt = salt
+ end
+
+ local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..self.state.iteration_count;
+ self.state["server_first_message"] = server_first_message;
+ return "challenge", server_first_message
+ else
+ -- we are processing client_final_message
+ local client_final_message = message;
+
+ self.state["channelbinding"], self.state["nonce"], self.state["proof"] = client_final_message:match("^c=(.*),r=(.*),.*p=(.*)");
+
+ if not self.state.proof or not self.state.nonce or not self.state.channelbinding then
+ return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message.";
+ end
+
+ if self.state.nonce ~= self.state.clientnonce..self.state.servernonce then
+ return "failure", "malformed-request", "Wrong nonce in client-final-message.";
+ end
+
+ local SaltedPassword = self.state.salted_password;
+ local ClientKey = HMAC_f(SaltedPassword, "Client Key")
+ local ServerKey = HMAC_f(SaltedPassword, "Server Key")
+ local StoredKey = H_f(ClientKey)
+ local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
+ local ClientSignature = HMAC_f(StoredKey, AuthMessage)
+ local ClientProof = binaryXOR(ClientKey, ClientSignature)
+ local ServerSignature = HMAC_f(ServerKey, AuthMessage)
+
+ if base64.encode(ClientProof) == self.state.proof then
+ local server_final_message = "v="..base64.encode(ServerSignature);
+ self["username"] = self.state.name;
+ return "success", server_final_message;
+ else
+ return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
+ end
+ end
+ end
+ return scram_hash;
+end
+
+function init(registerMechanism)
+ local function registerSCRAMMechanism(hash_name, hash, hmac_hash)
+ registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash));
+ end
+
+ registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);
+end
+
+return _M;
diff --git a/util/sasl_cyrus.lua b/util/sasl_cyrus.lua
new file mode 100644
index 00000000..7d35b5e4
--- /dev/null
+++ b/util/sasl_cyrus.lua
@@ -0,0 +1,176 @@
+-- sasl.lua v0.4
+-- Copyright (C) 2008-2009 Tobias Markmann
+--
+-- All rights reserved.
+--
+-- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+--
+-- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+-- * Neither the name of Tobias Markmann nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+--
+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local cyrussasl = require "cyrussasl";
+local log = require "util.logger".init("sasl_cyrus");
+local array = require "util.array";
+
+local tostring = tostring;
+local pairs, ipairs = pairs, ipairs;
+local t_insert, t_concat = table.insert, table.concat;
+local s_match = string.match;
+local setmetatable = setmetatable
+
+local keys = keys;
+
+local print = print
+local pcall = pcall
+local s_match, s_gmatch = string.match, string.gmatch
+
+local sasl_errstring = {
+ -- SASL result codes --
+ [1] = "another step is needed in authentication";
+ [0] = "successful result";
+ [-1] = "generic failure";
+ [-2] = "memory shortage failure";
+ [-3] = "overflowed buffer";
+ [-4] = "mechanism not supported";
+ [-5] = "bad protocol / cancel";
+ [-6] = "can't request info until later in exchange";
+ [-7] = "invalid parameter supplied";
+ [-8] = "transient failure (e.g., weak key)";
+ [-9] = "integrity check failed";
+ [-12] = "SASL library not initialized";
+
+ -- client only codes --
+ [2] = "needs user interaction";
+ [-10] = "server failed mutual authentication step";
+ [-11] = "mechanism doesn't support requested feature";
+
+ -- server only codes --
+ [-13] = "authentication failure";
+ [-14] = "authorization failure";
+ [-15] = "mechanism too weak for this user";
+ [-16] = "encryption needed to use mechanism";
+ [-17] = "One time use of a plaintext password will enable requested mechanism for user";
+ [-18] = "passphrase expired, has to be reset";
+ [-19] = "account disabled";
+ [-20] = "user not found";
+ [-23] = "version mismatch with plug-in";
+ [-24] = "remote authentication server unavailable";
+ [-26] = "user exists, but no verifier for user";
+
+ -- codes for password setting --
+ [-21] = "passphrase locked";
+ [-22] = "requested change was not needed";
+ [-27] = "passphrase is too weak for security policy";
+ [-28] = "user supplied passwords not permitted";
+};
+setmetatable(sasl_errstring, { __index = function() return "undefined error!" end });
+
+module "sasl_cyrus"
+
+local method = {};
+method.__index = method;
+local initialized = false;
+
+local function init(service_name)
+ if not initialized then
+ local st, errmsg = pcall(cyrussasl.server_init, service_name);
+ if st then
+ initialized = true;
+ else
+ log("error", "Failed to initialize Cyrus SASL: %s", errmsg);
+ end
+ end
+end
+
+-- create a new SASL object which can be used to authenticate clients
+function new(realm, service_name, app_name)
+ local sasl_i = {};
+
+ init(app_name or service_name);
+
+ sasl_i.realm = realm;
+ sasl_i.service_name = service_name;
+
+ local st, ret = pcall(cyrussasl.server_new, service_name, nil, realm, nil, nil)
+ if st then
+ sasl_i.cyrus = ret;
+ else
+ log("error", "Creating SASL server connection failed: %s", ret);
+ return nil;
+ end
+
+ if cyrussasl.set_canon_cb then
+ local c14n_cb = function (user)
+ local node = s_match(user, "^([^@]+)");
+ log("debug", "Canonicalizing username %s to %s", user, node)
+ return node
+ end
+ cyrussasl.set_canon_cb(sasl_i.cyrus, c14n_cb);
+ end
+
+ cyrussasl.setssf(sasl_i.cyrus, 0, 0xffffffff)
+ local s = setmetatable(sasl_i, method);
+ return s;
+end
+
+-- get a fresh clone with the same realm, profiles and forbidden mechanisms
+function method:clean_clone()
+ return new(self.realm, self.service_name)
+end
+
+-- set the forbidden mechanisms
+function method:forbidden( restrict )
+ log("warn", "Called method:forbidden. NOT IMPLEMENTED.")
+ return {}
+end
+
+-- get a list of possible SASL mechanims to use
+function method:mechanisms()
+ local mechanisms = {}
+ local cyrus_mechs = cyrussasl.listmech(self.cyrus, nil, "", " ", "")
+ for w in s_gmatch(cyrus_mechs, "[^ ]+") do
+ mechanisms[w] = true;
+ end
+ self.mechs = mechanisms
+ return array.collect(keys(mechanisms));
+end
+
+-- select a mechanism to use
+function method:select(mechanism)
+ self.mechanism = mechanism;
+ if not self.mechs then self:mechanisms(); end
+ return self.mechs[mechanism];
+end
+
+-- feed new messages to process into the library
+function method:process(message)
+ local err;
+ local data;
+
+ if self.mechanism then
+ err, data = cyrussasl.server_start(self.cyrus, self.mechanism, message or "")
+ else
+ err, data = cyrussasl.server_step(self.cyrus, message or "")
+ end
+
+ self.username = cyrussasl.get_username(self.cyrus)
+
+ if (err == 0) then -- SASL_OK
+ return "success", data
+ elseif (err == 1) then -- SASL_CONTINUE
+ return "challenge", data
+ elseif (err == -4) then -- SASL_NOMECH
+ log("debug", "SASL mechanism not available from remote end")
+ return "failure", "invalid-mechanism", "SASL mechanism not available"
+ elseif (err == -13) then -- SASL_BADAUTH
+ return "failure", "not-authorized", sasl_errstring[err];
+ else
+ log("debug", "Got SASL error condition %d: %s", err, sasl_errstring[err]);
+ return "failure", "undefined-condition", sasl_errstring[err];
+ end
+end
+
+return _M;
diff --git a/util/stanza.lua b/util/stanza.lua
index a457e619..08ef2c9a 100644
--- a/util/stanza.lua
+++ b/util/stanza.lua
@@ -67,7 +67,7 @@ end
function stanza_mt:text(text)
(self.last_add[#self.last_add] or self):add_direct_child(text);
- return self;
+ return self;
end
function stanza_mt:up()
@@ -97,7 +97,7 @@ end
function stanza_mt:get_child(name, xmlns)
for _, child in ipairs(self.tags) do
- if (not name or child.name == name)
+ if (not name or child.name == name)
and ((not xmlns and self.attr.xmlns == child.attr.xmlns)
or child.attr.xmlns == xmlns) then
@@ -107,13 +107,13 @@ function stanza_mt:get_child(name, xmlns)
end
function stanza_mt:child_with_name(name)
- for _, child in ipairs(self.tags) do
+ for _, child in ipairs(self.tags) do
if child.name == name then return child; end
end
end
function stanza_mt:child_with_ns(ns)
- for _, child in ipairs(self.tags) do
+ for _, child in ipairs(self.tags) do
if child.attr.xmlns == ns then return child; end
end
end
@@ -125,7 +125,6 @@ function stanza_mt:children()
local v = a[i]
if v then return v; end
end, self, i;
-
end
function stanza_mt:childtags()
local i = 0;
@@ -134,7 +133,6 @@ function stanza_mt:childtags()
local v = self.tags[i]
if v then return v; end
end, self.tags[1], i;
-
end
local xml_escape
@@ -193,6 +191,30 @@ function stanza_mt.get_text(t)
end
end
+function stanza_mt.get_error(stanza)
+ local type, condition, text;
+
+ local error_tag = stanza:get_child("error");
+ if not error_tag then
+ return nil, nil, nil;
+ end
+ type = error_tag.attr.type;
+
+ for child in error_tag:children() do
+ if child.attr.xmlns == xmlns_stanzas then
+ if not text and child.name == "text" then
+ text = child:get_text();
+ elseif not condition then
+ condition = child.name;
+ end
+ if condition and text then
+ break;
+ end
+ end
+ end
+ return type, condition or "undefined-condition", text or "";
+end
+
function stanza_mt.__add(s1, s2)
return s1:add_direct_child(s2);
end
@@ -322,7 +344,7 @@ if do_pretty_printing then
function stanza_mt.pretty_print(t)
local children_text = "";
for n, child in ipairs(t) do
- if type(child) == "string" then
+ if type(child) == "string" then
children_text = children_text .. xml_escape(child);
else
children_text = children_text .. child:pretty_print();
diff --git a/util/timer.lua b/util/timer.lua
index 05f91be8..fa1dd7c5 100644
--- a/util/timer.lua
+++ b/util/timer.lua
@@ -8,6 +8,9 @@
local ns_addtimer = require "net.server".addtimer;
+local event = require "net.server".event;
+local event_base = require "net.server".event_base;
+
local get_time = os.time;
local t_insert = table.insert;
local t_remove = table.remove;
@@ -19,33 +22,52 @@ local new_data = {};
module "timer"
-local function _add_task(delay, func)
- local current_time = get_time();
- delay = delay + current_time;
- if delay >= current_time then
- t_insert(new_data, {delay, func});
- else func(); end
-end
-
-add_task = _add_task;
-
-ns_addtimer(function()
- local current_time = get_time();
- if #new_data > 0 then
- for _, d in pairs(new_data) do
- t_insert(data, d);
+local _add_task;
+if not event then
+ function _add_task(delay, func)
+ local current_time = get_time();
+ delay = delay + current_time;
+ if delay >= current_time then
+ t_insert(new_data, {delay, func});
+ else
+ func();
end
- new_data = {};
end
-
- for i, d in pairs(data) do
- local t, func = d[1], d[2];
- if t <= current_time then
- data[i] = nil;
- local r = func(current_time);
- if type(r) == "number" then _add_task(r, func); end
+
+ ns_addtimer(function()
+ local current_time = get_time();
+ if #new_data > 0 then
+ for _, d in pairs(new_data) do
+ t_insert(data, d);
+ end
+ new_data = {};
end
+
+ for i, d in pairs(data) do
+ local t, func = d[1], d[2];
+ if t <= current_time then
+ data[i] = nil;
+ local r = func(current_time);
+ if type(r) == "number" then _add_task(r, func); end
+ end
+ end
+ end);
+else
+ local EVENT_LEAVE = (event.core and event.core.LEAVE) or -1;
+ function _add_task(delay, func)
+ local event_handle;
+ event_handle = event_base:addevent(nil, 0, function ()
+ local ret = func();
+ if ret then
+ return 0, ret;
+ elseif event_handle then
+ return EVENT_LEAVE;
+ end
+ end
+ , delay);
end
-end);
+end
+
+add_task = _add_task;
return _M;