Changeset

3065:0b8bd6f6a9c7

Merge 0.7->trunk
author Matthew Wild <mwild1@gmail.com>
date Thu, 20 May 2010 11:44:41 +0100
parents 3060:6c1cfb4cea3c (diff) 3064:596303990c7c (current diff)
children 3067:8dfa246a4413
files core/usermanager.lua
diffstat 20 files changed, 1212 insertions(+), 224 deletions(-) [+]
line wrap: on
line diff
--- a/core/configmanager.lua	Thu May 20 11:32:24 2010 +0100
+++ b/core/configmanager.lua	Thu May 20 11:44:41 2010 +0100
@@ -30,10 +30,11 @@
 -- When key not found in section, check key in global's section
 function section_mt(section_name)
 	return { __index = 	function (t, k)
-									local section = rawget(global_config, section_name);
-									if not section then return nil; end
-									return section[k];
-							end };
+					local section = rawget(global_config, section_name);
+					if not section then return nil; end
+					return section[k];
+				end
+	};
 end
 
 function getconfig()
@@ -112,16 +113,20 @@
 	function parsers.lua.load(data, filename)
 		local env;
 		-- The ' = true' are needed so as not to set off __newindex when we assign the functions below
-		env = setmetatable({ Host = true, host = true, VirtualHost = true, Component = true, component = true,
-							Include = true, include = true, RunScript = dofile }, { __index = function (t, k)
-												return rawget(_G, k) or
-														function (settings_table)
-															config[__currenthost or "*"][k] = settings_table;
-														end;
-										end,
-								__newindex = function (t, k, v)
-											set(env.__currenthost or "*", "core", k, v);
-										end});
+		env = setmetatable({
+			Host = true, host = true, VirtualHost = true,
+			Component = true, component = true,
+			Include = true, include = true, RunScript = dofile }, {
+				__index = function (t, k)
+					return rawget(_G, k) or
+						function (settings_table)
+							config[__currenthost or "*"][k] = settings_table;
+						end;
+				end,
+				__newindex = function (t, k, v)
+					set(env.__currenthost or "*", "core", k, v);
+				end
+		});
 		
 		rawset(env, "__currenthost", "*") -- Default is global
 		function env.VirtualHost(name)
--- a/core/eventmanager.lua	Thu May 20 11:32:24 2010 +0100
+++ b/core/eventmanager.lua	Thu May 20 11:44:41 2010 +0100
@@ -10,24 +10,18 @@
 local t_insert = table.insert;
 local ipairs = ipairs;
 
+local events = _G.prosody.events;
+
 module "eventmanager"
 
 local event_handlers = {};
 
 function add_event_hook(name, handler)
-	if not event_handlers[name] then
-		event_handlers[name] = {};
-	end
-	t_insert(event_handlers[name] , handler);
+	return events.add_handler(name, handler);
 end
 
 function fire_event(name, ...)
-	local event_handlers = event_handlers[name];
-	if event_handlers then
-		for name, handler in ipairs(event_handlers) do
-			handler(...);
-		end
-	end
+	return events.fire_event(name, ...);
 end
 
-return _M;
\ No newline at end of file
+return _M;
--- a/core/usermanager.lua	Thu May 20 11:32:24 2010 +0100
+++ b/core/usermanager.lua	Thu May 20 11:44:41 2010 +0100
@@ -18,82 +18,132 @@
 
 local require_provisioning = config.get("*", "core", "cyrus_require_provisioning") or false;
 
+local prosody = _G.prosody;
+
 module "usermanager"
 
+local new_default_provider;
+
+local function host_handler(host)
+	local host_session = hosts[host];
+	host_session.events.add_handler("item-added/auth-provider", function (provider)
+		if config.get(host, "core", "authentication") == provider.name then
+			host_session.users = provider;
+		end
+	end);
+	host_session.events.add_handler("item-removed/auth-provider", function (provider)
+		if host_session.users == provider then
+			host_session.users = new_default_provider(host);
+		end
+	end);
+	host_session.users = new_default_provider(host); -- Start with the default usermanager provider
+end
+prosody.events.add_handler("host-activated", host_handler);
+prosody.events.add_handler("component-activated", host_handler);
+
 local function is_cyrus(host) return config.get(host, "core", "sasl_backend") == "cyrus"; end
 
-function validate_credentials(host, username, password, method)
-	log("debug", "User '%s' is being validated", username);
-	if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end
-	local credentials = datamanager.load(username, host, "accounts") or {};
-
-	if method == nil then method = "PLAIN"; end
-	if method == "PLAIN" and credentials.password then -- PLAIN, do directly
+function new_default_provider(host)
+	local provider = { name = "default" };
+	
+	function provider:test_password(username, password)
+		if is_cyrus(host) then return nil, "Legacy auth not supported with Cyrus SASL."; end
+		local credentials = datamanager.load(username, host, "accounts") or {};
+	
 		if password == credentials.password then
 			return true;
 		else
 			return nil, "Auth failed. Invalid username or password.";
 		end
-  end
-	-- must do md5
-	-- make credentials md5
-	local pwd = credentials.password;
-	if not pwd then pwd = credentials.md5; else pwd = hashes.md5(pwd, true); end
-	-- make password md5
-	if method == "PLAIN" then
-		password = hashes.md5(password or "", true);
-	elseif method ~= "DIGEST-MD5" then
-		return nil, "Unsupported auth method";
+	end
+
+	function provider:get_password(username)
+		if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
+		return (datamanager.load(username, host, "accounts") or {}).password;
+	end
+	
+	function provider:set_password(username, password)
+		if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
+		local account = datamanager.load(username, host, "accounts");
+		if account then
+			account.password = password;
+			return datamanager.store(username, host, "accounts", account);
+		end
+		return nil, "Account not available.";
+	end
+
+	function provider:user_exists(username)
+		if not(require_provisioning) and is_cyrus(host) then return true; end
+		return datamanager.load(username, host, "accounts") ~= nil; -- FIXME also check for empty credentials
 	end
-	-- compare
-	if password == pwd then
-		return true;
-	else
-		return nil, "Auth failed. Invalid username or password.";
+
+	function provider:create_user(username, password)
+		if not(require_provisioning) and is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end
+		return datamanager.store(username, host, "accounts", {password = password});
+	end
+
+	function provider:get_supported_methods()
+		return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config
 	end
+
+	function provider:is_admin(jid)
+		local admins = config.get(host, "core", "admins");
+		if admins ~= config.get("*", "core", "admins") then
+			if type(admins) == "table" then
+				jid = jid_bare(jid);
+				for _,admin in ipairs(admins) do
+					if admin == jid then return true; end
+				end
+			elseif admins then
+				log("error", "Option 'admins' for host '%s' is not a table", host);
+			end
+		end
+		return is_admin(jid); -- Test whether it's a global admin instead
+	end
+	return provider;
+end
+
+function validate_credentials(host, username, password, method)
+	return hosts[host].users:test_password(username, password);
 end
 
 function get_password(username, host)
-	if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
-	return (datamanager.load(username, host, "accounts") or {}).password
+	return hosts[host].users:get_password(username);
 end
+
 function set_password(username, host, password)
-	if is_cyrus(host) then return nil, "Passwords unavailable for Cyrus SASL."; end
-	local account = datamanager.load(username, host, "accounts");
-	if account then
-		account.password = password;
-		return datamanager.store(username, host, "accounts", account);
-	end
-	return nil, "Account not available.";
+	return hosts[host].users:set_password(username, password);
 end
 
 function user_exists(username, host)
-	if not(require_provisioning) and is_cyrus(host) then return true; end
-	return datamanager.load(username, host, "accounts") ~= nil; -- FIXME also check for empty credentials
+	return hosts[host].users:user_exists(username);
 end
 
 function create_user(username, password, host)
-	if not(require_provisioning) and is_cyrus(host) then return nil, "Account creation/modification not available with Cyrus SASL."; end
-	return datamanager.store(username, host, "accounts", {password = password});
+	return hosts[host].users:create_user(username, password);
 end
 
 function get_supported_methods(host)
-	return {["PLAIN"] = true, ["DIGEST-MD5"] = true}; -- TODO this should be taken from the config
+	return hosts[host].users:get_supported_methods();
 end
 
 function is_admin(jid, host)
-	host = host or "*";
-	local admins = config.get(host, "core", "admins");
-	if host ~= "*" and admins == config.get("*", "core", "admins") then
+	if host and host ~= "*" then
+		return hosts[host].users:is_admin(jid);
+	else -- Test only whether this JID is a global admin
+		local admins = config.get("*", "core", "admins");
+		if type(admins) == "table" then
+			jid = jid_bare(jid);
+			for _,admin in ipairs(admins) do
+				if admin == jid then return true; end
+			end
+		elseif admins then
+			log("error", "Option 'admins' for host '%s' is not a table", host);
+		end
 		return nil;
 	end
-	if type(admins) == "table" then
-		jid = jid_bare(jid);
-		for _,admin in ipairs(admins) do
-			if admin == jid then return true; end
-		end
-	elseif admins then log("warn", "Option 'admins' for host '%s' is not a table", host); end
-	return nil;
 end
 
+_M.new_default_provider = new_default_provider;
+
 return _M;
--- a/net/xmppclient_listener.lua	Thu May 20 11:32:24 2010 +0100
+++ b/net/xmppclient_listener.lua	Thu May 20 11:44:41 2010 +0100
@@ -11,7 +11,7 @@
 local logger = require "logger";
 local log = logger.init("xmppclient_listener");
 local lxp = require "lxp"
-local init_xmlhandlers = require "core.xmlhandlers"
+local new_xmpp_stream = require "util.xmppstream".new;
 local sm_new_session = require "core.sessionmanager".new_session;
 
 local connlisteners_register = require "net.connlisteners".register;
@@ -72,23 +72,6 @@
 
 -- These are session methods --
 
-local function session_reset_stream(session)
-	-- Reset stream
-		local parser = lxp.new(init_xmlhandlers(session, stream_callbacks), "\1");
-		session.parser = parser;
-		
-		session.notopen = true;
-		
-		function session.data(conn, data)
-			local ok, err = parser:parse(data);
-			if ok then return; end
-			log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
-			session:close("xml-not-well-formed");
-		end
-		
-		return true;
-end
-
 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
 local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
 local function session_close(session, reason)
@@ -145,15 +128,29 @@
 			conn:setoption("keepalive", opt_keepalives);
 		end
 		
-		session.reset_stream = session_reset_stream;
 		session.close = session_close;
 		
-		session_reset_stream(session); -- Initialise, ready for use
+		local stream = new_xmpp_stream(session, stream_callbacks);
+		session.stream = stream;
+		
+		session.notopen = true;
+		
+		function session.reset_stream()
+			session.notopen = true;
+			session.stream:reset();
+		end
+		
+		function session.data(data)
+			local ok, err = stream:feed(data);
+			if ok then return; end
+			log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+			session:close("xml-not-well-formed");
+		end
 		
 		session.dispatch_stanza = stream_callbacks.handlestanza;
 	end
 	if data then
-		session.data(conn, data);
+		session.data(data);
 	end
 end
 	
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_offline.lua	Thu May 20 11:44:41 2010 +0100
@@ -0,0 +1,56 @@
+-- Prosody IM
+-- Copyright (C) 2008-2009 Matthew Wild
+-- Copyright (C) 2008-2009 Waqas Hussain
+-- 
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local datamanager = require "util.datamanager";
+local st = require "util.stanza";
+local datetime = require "util.datetime";
+local ipairs = ipairs;
+local jid_split = require "util.jid".split;
+
+module:add_feature("msgoffline");
+
+module:hook("message/offline/store", function(event)
+	local origin, stanza = event.origin, event.stanza;
+	local to = stanza.attr.to;
+	local node, host;
+	if to then
+		node, host = jid_split(to)
+	else
+		node, host = origin.username, origin.host;
+	end
+	
+	stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy();
+	local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza));
+	stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+	
+	return true;
+end);
+
+module:hook("message/offline/broadcast", function(event)
+	local origin = event.origin;
+	local node, host = origin.username, origin.host;
+	
+	local data = datamanager.list_load(node, host, "offline");
+	if not data then return true; end
+	for _, stanza in ipairs(data) do
+		stanza = st.deserialize(stanza);
+		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = stanza.attr.stamp}):up(); -- XEP-0203
+		stanza:tag("x", {xmlns = "jabber:x:delay", from = host, stamp = stanza.attr.stamp_legacy}):up(); -- XEP-0091 (deprecated)
+		stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
+		origin.send(stanza);
+	end
+	return true;
+end);
+
+module:hook("message/offline/delete", function(event)
+	local origin = event.origin;
+	local node, host = origin.username, origin.host;
+
+	return datamanager.list_store(node, host, "offline", nil);
+end);
--- a/plugins/mod_posix.lua	Thu May 20 11:32:24 2010 +0100
+++ b/plugins/mod_posix.lua	Thu May 20 11:44:41 2010 +0100
@@ -54,16 +54,16 @@
 	end);
 
 -- Don't even think about it!
-module:add_event_hook("server-starting", function ()
-		local suid = module:get_option("setuid");
-		if not suid or suid == 0 or suid == "root" then
-			if pposix.getuid() == 0 and not module:get_option("run_as_root") then
-				module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
-				module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
-				prosody.shutdown("Refusing to run as root");
-			end
+if not prosody.start_time then -- server-starting
+	local suid = module:get_option("setuid");
+	if not suid or suid == 0 or suid == "root" then
+		if pposix.getuid() == 0 and not module:get_option("run_as_root") then
+			module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!");
+			module:log("error", "For more information on running Prosody as root, see http://prosody.im/doc/root");
+			prosody.shutdown("Refusing to run as root");
 		end
-	end);
+	end
+end
 
 local pidfile;
 local pidfile_handle;
@@ -141,7 +141,9 @@
 			write_pidfile();
 		end
 	end
-	module:add_event_hook("server-starting", daemonize_server);
+	if not prosody.start_time then -- server-starting
+		daemonize_server();
+	end
 else
 	-- Not going to daemonize, so write the pid of this process
 	write_pidfile();
--- a/plugins/muc/muc.lib.lua	Thu May 20 11:32:24 2010 +0100
+++ b/plugins/muc/muc.lib.lua	Thu May 20 11:44:41 2010 +0100
@@ -122,9 +122,13 @@
 		local history = self._data['history'];
 		if not history then history = {}; self._data['history'] = history; end
 		stanza = st.clone(stanza);
-		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
+		stanza.attr.to = "";
+		local stamp = datetime.datetime();
+		local chars = #tostring(stanza);
+		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203
 		stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
-		t_insert(history, st.preserialize(stanza));
+		local entry = { stanza = stanza, stamp = stamp };
+		t_insert(history, entry);
 		while #history > history_length do t_remove(history, 1) end
 	end
 end
@@ -151,12 +155,46 @@
 		end
 	end
 end
-function room_mt:send_history(to)
+function room_mt:send_history(to, stanza)
 	local history = self._data['history']; -- send discussion history
 	if history then
-		for _, msg in ipairs(history) do
-			msg = st.deserialize(msg);
-			msg.attr.to=to;
+		local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
+		local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
+		
+		local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
+		if maxchars then maxchars = math.floor(maxchars); end
+		
+		local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
+		if not history_tag then maxstanzas = 20; end
+
+		local seconds = history_tag and tonumber(history_tag.attr.seconds);
+		if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end
+
+		local since = history_tag and history_tag.attr.since;
+		if since and not since:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%dZ$") then since = nil; end -- FIXME timezone support
+		if seconds and (not since or since < seconds) then since = seconds; end
+
+		local n = 0;
+		local charcount = 0;
+		local stanzacount = 0;
+		
+		for i=#history,1,-1 do
+			local entry = history[i];
+			if maxchars then
+				if not entry.chars then
+					entry.stanza.attr.to = "";
+					entry.chars = #tostring(entry.stanza);
+				end
+				charcount = charcount + entry.chars + #to;
+				if charcount > maxchars then break; end
+			end
+			if since and since > entry.stamp then break; end
+			if n + 1 > maxstanzas then break; end
+			n = n + 1;
+		end
+		for i=#history-n+1,#history do
+			local msg = history[i].stanza;
+			msg.attr.to = to;
 			self:_route_stanza(msg);
 		end
 	end
@@ -319,12 +357,7 @@
 								:tag("item", {affiliation=affiliation or "none", role=role or "none"}):up()
 								:tag("status", {code='110'}));
 						end
-						if self._data.whois == 'anyone' then -- non-anonymous?
-							self:_route_stanza(st.stanza("message", {from=to, to=from, type='groupchat'})
-								:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
-								:tag("status", {code='100'}));
-						end
-						self:send_history(from);
+						self:send_history(from, stanza);
 					else -- banned
 						local reply = st.error_reply(stanza, "auth", "forbidden"):up();
 						reply.tags[1].attr.code = "403";
@@ -751,7 +784,7 @@
 function room_mt:set_role(actor, occupant_jid, role, callback, reason)
 	if role == "none" then role = nil; end
 	if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end
-	if self:get_role(self._jid_nick[actor]) ~= "moderator" then return nil, "cancel", "not-allowed"; end
+	if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end
 	local occupant = self._occupants[occupant_jid];
 	if not occupant then return nil, "modify", "not-acceptable"; end
 	if occupant.affiliation == "owner" or occupant.affiliation == "admin" then return nil, "cancel", "not-allowed"; end
@@ -804,6 +837,9 @@
 				end
 			end
 		end
+		if self._data.whois == 'anyone' then
+		    muc_child:tag('status', { code = '100' });
+		end
 	end
 	self:route_stanza(stanza);
 	if muc_child then
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/storage/ejabberdstore.lib.lua	Thu May 20 11:44:41 2010 +0100
@@ -0,0 +1,190 @@
+
+local handlers = {};
+
+handlers.accounts = {
+	get = function(self, user)
+		local select = self:query("select password from users where username=?", user);
+		local row = select and select:fetch();
+		if row then return { password = row[1] }; end
+	end;
+	set = function(self, user, data)
+		if data and data.password then
+			return self:modify("update users set password=? where username=?", data.password, user)
+				or self:modify("insert into users (username, password) values (?, ?)", user, data.password);
+		else
+			return self:modify("delete from users where username=?", user);
+		end
+	end;
+};
+handlers.vcard = {
+	get = function(self, user)
+		local select = self:query("select vcard from vcard where username=?", user);
+		local row = select and select:fetch();
+		if row then return parse_xml(row[1]); end
+	end;
+	set = function(self, user, data)
+		if data then
+			data = unparse_xml(data);
+			return self:modify("update vcard set vcard=? where username=?", data, user)
+				or self:modify("insert into vcard (username, vcard) values (?, ?)", user, data);
+		else
+			return self:modify("delete from vcard where username=?", user);
+		end
+	end;
+};
+handlers.private = {
+	get = function(self, user)
+		local select = self:query("select namespace,data from private_storage where username=?", user);
+		if select then
+			local data = {};
+			for row in select:rows() do
+				data[row[1]] = parse_xml(row[2]);
+			end
+			return data;
+		end
+	end;
+	set = function(self, user, data)
+		if data then
+			self:modify("delete from private_storage where username=?", user);
+			for namespace,text in pairs(data) do
+				self:modify("insert into private_storage (username, namespace, data) values (?, ?, ?)", user, namespace, unparse_xml(text));
+			end
+			return true;
+		else
+			return self:modify("delete from private_storage where username=?", user);
+		end
+	end;
+	-- TODO map_set, map_get
+};
+local subscription_map = { N = "none", B = "both", F = "from", T = "to" };
+local subscription_map_reverse = { none = "N", both = "B", from = "F", to = "T" };
+handlers.roster = {
+	get = function(self, user)
+		local select = self:query("select jid,nick,subscription,ask,server,subscribe,type from rosterusers where username=?", user);
+		if select then
+			local roster = { pending = {} };
+			for row in select:rows() do
+				local jid,nick,subscription,ask,server,subscribe,typ = unpack(row);
+				local item = { groups = {} };
+				if nick == "" then nick = nil; end
+				item.nick = nick;
+				item.subscription = subscription_map[subscription];
+				if ask == "N" then ask = nil;
+				elseif ask == "O" then ask = "subscribe"
+				elseif ask == "I" then roster.pending[jid] = true; ask = nil;
+				elseif ask == "B" then roster.pending[jid] = true; ask = "subscribe";
+				else module:log("debug", "bad roster_item.ask: %s", ask); ask = nil; end
+				item.ask = ask;
+				roster[jid] = item;
+			end
+			
+			select = self:query("select jid,grp from rostergroups where username=?", user);
+			if select then
+				for row in select:rows() do
+					local jid,grp = unpack(rows);
+					if roster[jid] then roster[jid].groups[grp] = true; end
+				end
+			end
+			select = self:query("select version from roster_version where username=?", user);
+			local row = select and select:fetch();
+			if row then
+				roster[false] = { version = row[1]; };
+			end
+			return roster;
+		end
+	end;
+	set = function(self, user, data)
+		if data and next(data) ~= nil then
+			self:modify("delete from rosterusers where username=?", user);
+			self:modify("delete from rostergroups where username=?", user);
+			self:modify("delete from roster_version where username=?", user);
+			local done = {};
+			local pending = data.pending or {};
+			for jid,item in pairs(data) do
+				if jid and jid ~= "pending" then
+					local subscription = subscription_map_reverse[item.subscription];
+					local ask;
+					if pending[jid] then
+						if item.ask then ask = "B"; else ask = "I"; end
+					else
+						if item.ask then ask = "O"; else ask = "N"; end
+					end
+					local r = self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?, '', '', '')", user, jid, item.nick or "", subscription, ask);
+					if not r then module:log("debug", "--- :( %s", tostring(r)); end
+					done[jid] = true;
+					for group in pairs(item.groups) do
+						self:modify("insert into rostergroups (username,jid,grp) values (?, ?, ?)", user, jid, group);
+					end
+				end
+			end
+			for jid in pairs(pending) do
+				if not done[jid] then
+					self:modify("insert into rosterusers (username,jid,nick,subscription,ask,askmessage,server,subscribe) values (?, ?, ?, ?, ?. ''. ''. '')", user, jid, "", "N", "I");
+				end
+			end
+			local version = data[false] and data[false].version;
+			if version then
+				self:modify("insert into roster_version (username,version) values (?, ?)", user, version);
+			end
+			return true;
+		else
+			self:modify("delete from rosterusers where username=?", user);
+			self:modify("delete from rostergroups where username=?", user);
+			self:modify("delete from roster_version where username=?", user);
+		end
+	end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:prepare(sql)
+	module:log("debug", "query: %s", sql);
+	local err;
+	if not self.sqlcache then self.sqlcache = {}; end
+	local r = self.sqlcache[sql];
+	if r then return r; end
+	r, err = self.database:prepare(sql);
+	if not r then error("Unable to prepare SQL statement: "..err); end
+	self.sqlcache[sql] = r;
+	return r;
+end
+
+function driver:query(sql, ...)
+	local stmt = self:prepare(sql);
+	if stmt:execute(...) then return stmt; end
+end
+function driver:modify(sql, ...)
+	local stmt = self:query(sql, ...);
+	if stmt and stmt:affected() > 0 then return stmt; end
+end
+
+function driver:open(host, datastore, typ)
+	local cache_key = host.." "..datastore;
+	if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+	local instance = setmetatable({}, self);
+	instance.host = host;
+	instance.datastore = datastore;
+	local handler = handlers[datastore];
+	if not handler then return nil; end
+	for key,val in pairs(handler) do
+		instance[key] = val;
+	end
+	if instance.init then instance:init(); end
+	self.ds_cache[cache_key] = instance;
+	return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new(dbtype, dbname, ...)
+	local instance = setmetatable({}, driver);
+	instance.__index = instance;
+	instance.database = get_database(dbtype, dbname, ...);
+	instance.ds_cache = {};
+	return instance;
+end
+
+return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/storage/mod_storage.lua	Thu May 20 11:44:41 2010 +0100
@@ -0,0 +1,83 @@
+
+module:set_global();
+
+local cache = { data = {} };
+function cache:get(key) return self.data[key]; end
+function cache:set(key, val) self.data[key] = val; return val; end
+
+local DBI = require "DBI";
+function get_database(driver, db, ...)
+	local uri = "dbi:"..driver..":"..db;
+	return cache:get(uri) or cache:set(uri, (function(...)
+		module:log("debug", "Opening database: %s", uri);
+		prosody.unlock_globals();
+		local dbh = assert(DBI.Connect(...));
+		prosody.lock_globals();
+		dbh:autocommit(true)
+		return dbh;
+	end)(driver, db, ...));
+end
+
+local st = require "util.stanza";
+local _parse_xml = module:require("xmlparse");
+parse_xml_real = _parse_xml;
+function parse_xml(str)
+	local s = _parse_xml(str);
+	if s and not s.gsub then
+		return st.preserialize(s);
+	end
+end
+function unparse_xml(s)
+	return tostring(st.deserialize(s));
+end
+
+local drivers = {};
+
+--local driver = module:require("sqlbasic").new("SQLite3", "hello.sqlite");
+local option_datastore = module:get_option("datastore");
+local option_datastore_params = module:get_option("datastore_params") or {};
+if option_datastore then
+	local driver = module:require(option_datastore).new(unpack(option_datastore_params));
+	table.insert(drivers, driver);
+end
+
+local datamanager = require "util.datamanager";
+local olddm = {};
+local dm = {};
+for key,val in pairs(datamanager) do olddm[key] = val; end
+
+do -- driver based on old datamanager
+	local dmd = {};
+	dmd.__index = dmd;
+	function dmd:open(host, datastore)
+		return setmetatable({ host = host, datastore = datastore }, dmd);
+	end
+	function dmd:get(user) return olddm.load(user, self.host, self.datastore); end
+	function dmd:set(user, data) return olddm.store(user, self.host, self.datastore, data); end
+	table.insert(drivers, dmd);
+end
+
+local function open(...)
+	for _,driver in pairs(drivers) do
+		local ds = driver:open(...);
+		if ds then return ds; end
+	end
+end
+
+local _data_path;
+--function dm.set_data_path(path) _data_path = path; end
+--function dm.add_callback(...) end
+--function dm.remove_callback(...) end
+--function dm.getpath(...) end
+function dm.load(username, host, datastore)
+	local x = open(host, datastore);
+	return x:get(username);
+end
+function dm.store(username, host, datastore, data)
+	return open(host, datastore):set(username, data);
+end
+--function dm.list_append(...) return driver:list_append(...); end
+--function dm.list_store(...) return driver:list_store(...); end
+--function dm.list_load(...) return driver:list_load(...); end
+
+for key,val in pairs(dm) do datamanager[key] = val; end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/storage/sqlbasic.lib.lua	Thu May 20 11:44:41 2010 +0100
@@ -0,0 +1,97 @@
+
+-- Basic SQL driver
+-- This driver stores data as simple key-values
+
+local ser = require "util.serialization".serialize;
+local deser = function(data)
+	module:log("debug", "deser: %s", tostring(data));
+	if not data then return nil; end
+	local f = loadstring("return "..data);
+	if not f then return nil; end
+	setfenv(f, {});
+	local s, d = pcall(f);
+	if not s then return nil; end
+	return d;
+end;
+
+local driver = {};
+driver.__index = driver;
+
+driver.item_table = "item";
+driver.list_table = "list";
+
+function driver:prepare(sql)
+	module:log("debug", "query: %s", sql);
+	local err;
+	if not self.sqlcache then self.sqlcache = {}; end
+	local r = self.sqlcache[sql];
+	if r then return r; end
+	r, err = self.connection:prepare(sql);
+	if not r then error("Unable to prepare SQL statement: "..err); end
+	self.sqlcache[sql] = r;
+	return r;
+end
+
+function driver:load(username, host, datastore)
+	local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?");
+	select:execute(username, host, datastore);
+	local row = select:fetch();
+	return row and deser(row[1]) or nil;
+end
+
+function driver:store(username, host, datastore, data)
+	if not data or next(data) == nil then
+		local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?");
+		delete:execute(username, host, datastore);
+		return true;
+	else
+		local d = self:load(username, host, datastore);
+		if d then -- update
+			local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?");
+			return update:execute(ser(data), username, host, datastore);
+		else -- insert
+			local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)");
+			return insert:execute(username, host, datastore, ser(data));
+		end
+	end
+end
+
+function driver:list_append(username, host, datastore, data)
+	if not data then return; end
+	local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)");
+	return insert:execute(username, host, datastore, ser(data));
+end
+
+function driver:list_store(username, host, datastore, data)
+	-- remove existing data
+	local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?");
+	delete:execute(username, host, datastore);
+	if data and next(data) ~= nil then
+		-- add data
+		for _, d in ipairs(data) do
+			self:list_append(username, host, datastore, ser(d));
+		end
+	end
+	return true;
+end
+
+function driver:list_load(username, host, datastore)
+	local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?");
+	select:execute(username, host, datastore);
+	local r = {};
+	for row in select:rows() do
+		table.insert(r, deser(row[1]));
+	end
+	return r;
+end
+
+local _M = {};
+function _M.new(dbtype, dbname, ...)
+	local d = {};
+	setmetatable(d, driver);
+	local dbh = get_database(dbtype, dbname, ...);
+	--d:set_connection(dbh);
+	d.connection = dbh;
+	return d;
+end
+return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/storage/xep227store.lib.lua	Thu May 20 11:44:41 2010 +0100
@@ -0,0 +1,168 @@
+
+local st = require "util.stanza";
+
+local function getXml(user, host)
+	local jid = user.."@"..host;
+	local path = "data/"..jid..".xml";
+	local f = io.open(path);
+	if not f then return; end
+	local s = f:read("*a");
+	return parse_xml_real(s);
+end
+local function setXml(user, host, xml)
+	local jid = user.."@"..host;
+	local path = "data/"..jid..".xml";
+	if xml then
+		local f = io.open(path, "w");
+		if not f then return; end
+		local s = tostring(xml);
+		f:write(s);
+		f:close();
+		return true;
+	else
+		return os.remove(path);
+	end
+end
+local function getUserElement(xml)
+	if xml and xml.name == "server-data" then
+		local host = xml.tags[1];
+		if host and host.name == "host" then
+			local user = host.tags[1];
+			if user and user.name == "user" then
+				return user;
+			end
+		end
+	end
+end
+local function createOuterXml(user, host)
+	return st.stanza("server-data", {xmlns='http://www.xmpp.org/extensions/xep-0227.html#ns'})
+		:tag("host", {jid=host})
+			:tag("user", {name = user});
+end
+local function removeFromArray(array, value)
+	for i,item in ipairs(array) do
+		if item == value then
+			table.remove(array, i);
+			return;
+		end
+	end
+end
+local function removeStanzaChild(s, child)
+	removeFromArray(s.tags, child);
+	removeFromArray(s, child);
+end
+
+local handlers = {};
+
+handlers.accounts = {
+	get = function(self, user)
+		local user = getUserElement(getXml(user, self.host));
+		if user and user.attr.password then
+			return { password = user.attr.password };
+		end
+	end;
+	set = function(self, user, data)
+		if data and data.password then
+			local xml = getXml(user, self.host);
+			if not xml then xml = createOuterXml(user, self.host); end
+			local usere = getUserElement(xml);
+			usere.attr.password = data.password;
+			return setXml(user, self.host, xml);
+		else
+			return setXml(user, self.host, nil);
+		end
+	end;
+};
+handlers.vcard = {
+	get = function(self, user)
+		local user = getUserElement(getXml(user, self.host));
+		if user then
+			local vcard = user:get_child("vCard", 'vcard-temp');
+			if vcard then
+				return st.preserialize(vcard);
+			end
+		end
+	end;
+	set = function(self, user, data)
+		local xml = getXml(user, self.host);
+		local usere = xml and getUserElement(xml);
+		if usere then
+			local vcard = usere:get_child("vCard", 'vcard-temp');
+			if vcard then
+				removeStanzaChild(usere, vcard);
+			elseif not data then
+				return true;
+			end
+			if data then
+				vcard = st.deserialize(data);
+				usere:add_child(vcard);
+			end
+			return setXml(user, self.host, xml);
+		end
+		return true;
+	end;
+};
+handlers.private = {
+	get = function(self, user)
+		local user = getUserElement(getXml(user, self.host));
+		if user then
+			local private = user:get_child("query", "jabber:iq:private");
+			if private then
+				local r = {};
+				for _, tag in ipairs(private.tags) do
+					r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
+				end
+				return r;
+			end
+		end
+	end;
+	set = function(self, user, data)
+		local xml = getXml(user, self.host);
+		local usere = xml and getUserElement(xml);
+		if usere then
+			local private = usere:get_child("query", 'jabber:iq:private');
+			if private then removeStanzaChild(usere, private); end
+			if data and next(data) ~= nil then
+				private = st.stanza("query", {xmlns='jabber:iq:private'});
+				for _,tag in pairs(data) do
+					private:add_child(st.deserialize(tag));
+				end
+				usere:add_child(private);
+			end
+			return setXml(user, self.host, xml);
+		end
+		return true;
+	end;
+};
+
+-----------------------------
+local driver = {};
+driver.__index = driver;
+
+function driver:open(host, datastore, typ)
+	local cache_key = host.." "..datastore;
+	if self.ds_cache[cache_key] then return self.ds_cache[cache_key]; end
+	local instance = setmetatable({}, self);
+	instance.host = host;
+	instance.datastore = datastore;
+	local handler = handlers[datastore];
+	if not handler then return nil; end
+	for key,val in pairs(handler) do
+		instance[key] = val;
+	end
+	if instance.init then instance:init(); end
+	self.ds_cache[cache_key] = instance;
+	return instance;
+end
+
+-----------------------------
+local _M = {};
+
+function _M.new()
+	local instance = setmetatable({}, driver);
+	instance.__index = instance;
+	instance.ds_cache = {};
+	return instance;
+end
+
+return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/storage/xmlparse.lib.lua	Thu May 20 11:44:41 2010 +0100
@@ -0,0 +1,56 @@
+
+local st = require "util.stanza";
+
+-- XML parser
+local parse_xml = (function()
+	local entity_map = setmetatable({
+		["amp"] = "&";
+		["gt"] = ">";
+		["lt"] = "<";
+		["apos"] = "'";
+		["quot"] = "\"";
+	}, {__index = function(_, s)
+			if s:sub(1,1) == "#" then
+				if s:sub(2,2) == "x" then
+					return string.char(tonumber(s:sub(3), 16));
+				else
+					return string.char(tonumber(s:sub(2)));
+				end
+			end
+		end
+	});
+	local function xml_unescape(str)
+		return (str:gsub("&(.-);", entity_map));
+	end
+	local function parse_tag(s)
+		local name,sattr=(s):gmatch("([^%s]+)(.*)")();
+		local attr = {};
+		for a,b in (sattr):gmatch("([^=%s]+)=['\"]([^'\"]*)['\"]") do attr[a] = xml_unescape(b); end
+		return name, attr;
+	end
+	return function(xml)
+		local stanza = st.stanza("root");
+		local regexp = "<([^>]*)>([^<]*)";
+		for elem, text in xml:gmatch(regexp) do
+			if elem:sub(1,1) == "!" or elem:sub(1,1) == "?" then -- neglect comments and processing-instructions
+			elseif elem:sub(1,1) == "/" then -- end tag
+				elem = elem:sub(2);
+				stanza:up(); -- TODO check for start-end tag name match
+			elseif elem:sub(-1,-1) == "/" then -- empty tag
+				elem = elem:sub(1,-2);
+				local name,attr = parse_tag(elem);
+				stanza:tag(name, attr):up();
+			else -- start tag
+				local name,attr = parse_tag(elem);
+				stanza:tag(name, attr);
+			end
+			if #text ~= 0 then -- text
+				stanza:text(xml_unescape(text));
+			end
+		end
+		return stanza.tags[1];
+	end
+end)();
+-- end of XML parser
+
+return parse_xml;
--- a/prosody	Thu May 20 11:32:24 2010 +0100
+++ b/prosody	Thu May 20 11:44:41 2010 +0100
@@ -22,9 +22,6 @@
 	package.cpath = CFG_SOURCEDIR.."/?.so;"..package.cpath;
 end
 
-package.path = package.path..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.lua";
-package.cpath = package.cpath..";"..(CFG_SOURCEDIR or ".").."/fallbacks/?.so";
-
 -- Substitute ~ with path to home directory in data path
 if CFG_DATADIR then
 	if os.getenv("HOME") then
@@ -32,6 +29,10 @@
 	end
 end
 
+-- Global 'prosody' object
+prosody = { events = require "util.events".new(); };
+local prosody = prosody;
+
 -- Load the config-parsing module
 config = require "core.configmanager"
 
@@ -155,10 +156,6 @@
 	full_sessions = {};
 	hosts = {};
 
-	-- Global 'prosody' object
-	prosody = {};
-	local prosody = prosody;
-	
 	prosody.bare_sessions = bare_sessions;
 	prosody.full_sessions = full_sessions;
 	prosody.hosts = hosts;
@@ -168,8 +165,6 @@
 	
 	prosody.arg = _G.arg;
 
-	prosody.events = require "util.events".new();
-	
 	prosody.platform = "unknown";
 	if os.getenv("WINDIR") then
 		prosody.platform = "windows";
@@ -200,7 +195,6 @@
 	-- Function to reopen logfiles
 	function prosody.reopen_logfiles()
 		log("info", "Re-opening log files");
-		eventmanager.fire_event("reopen-log-files"); -- Handled by appropriate log sinks
 		prosody.events.fire_event("reopen-log-files");
 	end
 
@@ -291,9 +285,9 @@
 function load_secondary_libraries()
 	--- Load and initialise core modules
 	require "util.import"
+	require "util.xmppstream"
 	require "core.xmlhandlers"
 	require "core.rostermanager"
-	require "core.eventmanager"
 	require "core.hostmanager"
 	require "core.modulemanager"
 	require "core.usermanager"
@@ -337,7 +331,6 @@
 function prepare_to_start()
 	log("debug", "Prosody is using the %s backend for connection handling", server.get_backend());
 	-- Signal to modules that we are ready to start
-	eventmanager.fire_event("server-starting");
 	prosody.events.fire_event("server-starting");
 
 	-- start listening on sockets
@@ -455,14 +448,12 @@
 init_global_protection();
 prepare_to_start();
 
-eventmanager.fire_event("server-started");
 prosody.events.fire_event("server-started");
 
 loop();
 
 log("info", "Shutting down...");
 cleanup();
-eventmanager.fire_event("server-stopped");
 prosody.events.fire_event("server-stopped");
 log("info", "Shutdown complete");
 
--- a/prosodyctl	Thu May 20 11:32:24 2010 +0100
+++ b/prosodyctl	Thu May 20 11:44:41 2010 +0100
@@ -29,6 +29,14 @@
 	end
 end
 
+-- Global 'prosody' object
+prosody = {
+	hosts = {},
+	events = require "util.events".new(),
+	platform = "posix"
+};
+local prosody = prosody;
+
 config = require "core.configmanager"
 
 do
@@ -63,8 +71,6 @@
 	os.exit(1);
 end
 
-prosody = { hosts = {}, events = events, platform = "posix" };
-
 local data_path = config.get("*", "core", "data_path") or CFG_DATADIR or "data";
 require "util.datamanager".set_data_path(data_path);
 
@@ -114,12 +120,14 @@
 		["not-running"] = "Prosody is not running";
 		}, { __index = function (t,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end });
 
-local events = require "util.events".new();
-
 hosts = prosody.hosts;
 
+local function make_host(hostname)
+	return { events = prosody.events, users = require "core.usermanager".new_default_provider(hostname) };
+end
+
 for hostname, config in pairs(config.getconfig()) do
-	hosts[hostname] = { events = events };
+	hosts[hostname] = make_host(hostname);
 end
 	
 require "core.modulemanager"
@@ -231,16 +239,23 @@
 		return 1;
 	end
 	
+	if not hosts[host] then
+		show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
+		show_warning("The user will not be able to log in until this is changed.");
+		hosts[host] = make_host(host);
+	elseif config.get(host, "core", "authentication")
+		and config.get(host, "core", "authentication") ~= "default" then
+		show_warning("The host '%s' is configured to use the '%s' authentication provider", host,
+			config.get(host, "core", "authentication"));
+		show_warning("prosodyctl currently only supports the default provider, sorry :(");
+		return 1;
+	end
+	
 	if prosodyctl.user_exists{ user = user, host = host } then
 		show_message [[That user already exists]];
 		return 1;
 	end
 	
-	if not hosts[host] then
-		show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
-		show_warning("The user will not be able to log in until this is changed.");
-	end
-	
 	local password = read_password();
 	if not password then return 1; end
 	
@@ -269,6 +284,18 @@
 		return 1;
 	end
 	
+	if not hosts[host] then
+		show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
+		show_warning("The user will not be able to log in until this is changed.");
+		hosts[host] = make_host(host);
+	elseif config.get(host, "core", "authentication")
+		and config.get(host, "core", "authentication") ~= "default" then
+		show_warning("The host '%s' is configured to use the '%s' authentication provider", host,
+			config.get(host, "core", "authentication"));
+		show_warning("prosodyctl currently only supports the default provider, sorry :(");
+		return 1;
+	end
+	
 	if not prosodyctl.user_exists { user = user, host = host } then
 		show_message [[That user does not exist, use prosodyctl adduser to create a new user]]
 		return 1;
@@ -302,6 +329,18 @@
 		return 1;
 	end
 	
+	if not hosts[host] then
+		show_warning("The host '%s' is not listed in the configuration file (or is not enabled).", host)
+		show_warning("The user will not be able to log in until this is changed.");
+		hosts[host] = make_host(host);
+	elseif config.get(host, "core", "authentication")
+		and config.get(host, "core", "authentication") ~= "default" then
+		show_warning("The host '%s' is configured to use the '%s' authentication provider", host,
+			config.get(host, "core", "authentication"));
+		show_warning("prosodyctl currently only supports the default provider, sorry :(");
+		return 1;
+	end
+
 	if not prosodyctl.user_exists { user = user, host = host } then
 		show_message [[That user does not exist on this server]]
 		return 1;
--- a/util/sasl.lua	Thu May 20 11:32:24 2010 +0100
+++ b/util/sasl.lua	Thu May 20 11:44:41 2010 +0100
@@ -41,27 +41,6 @@
 state = false : disabled
 state = true : enabled
 state = nil : non-existant
-
-plain:
-	function(username, realm)
-		return password, state;
-	end
-
-plain-test:
-	function(username, realm, password)
-		return true or false, state;
-	end
-
-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 method = {};
--- a/util/sasl/anonymous.lua	Thu May 20 11:32:24 2010 +0100
+++ b/util/sasl/anonymous.lua	Thu May 20 11:44:41 2010 +0100
@@ -1,5 +1,5 @@
 -- sasl.lua v0.4
--- Copyright (C) 2008-2009 Tobias Markmann
+-- Copyright (C) 2008-2010 Tobias Markmann
 --
 --    All rights reserved.
 --
@@ -20,6 +20,16 @@
 
 --=========================
 --SASL ANONYMOUS according to RFC 4505
+
+--[[
+Supported Authentication Backends
+
+anonymous:
+	function(username, realm)
+		return true; --for normal usage just return true; if you don't like the supplied username you can return false.
+	end
+]]
+
 local function anonymous(self, message)
 	local username;
 	repeat
--- a/util/sasl/digest-md5.lua	Thu May 20 11:32:24 2010 +0100
+++ b/util/sasl/digest-md5.lua	Thu May 20 11:44:41 2010 +0100
@@ -1,5 +1,5 @@
 -- sasl.lua v0.4
--- Copyright (C) 2008-2009 Tobias Markmann
+-- Copyright (C) 2008-2010 Tobias Markmann
 --
 --    All rights reserved.
 --
@@ -29,6 +29,21 @@
 --=========================
 --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
 
--- a/util/sasl/plain.lua	Thu May 20 11:32:24 2010 +0100
+++ b/util/sasl/plain.lua	Thu May 20 11:44:41 2010 +0100
@@ -1,5 +1,5 @@
 -- sasl.lua v0.4
--- Copyright (C) 2008-2009 Tobias Markmann
+-- Copyright (C) 2008-2010 Tobias Markmann
 --
 --    All rights reserved.
 --
@@ -19,6 +19,26 @@
 
 -- ================================
 -- 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
+	
+plain-hashed:
+	function(username, realm)
+		return hashed_password, hash_function, state;
+	end
+]]
+
 local function plain(self, message)
 	if not message then
 		return "failure", "malformed-request";
@@ -46,6 +66,10 @@
 		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);
+	elseif self.profile.plain_hashed then
+		local hashed_password, hash_f;
+		hashed_password, hash_f, state = self.profile.plain_hashed(authentication, self.realm);
+		if hashed_password == hash_f(password) then correct = true; else correct = false; end
 	end
 
 	self.username = authentication
@@ -61,7 +85,7 @@
 end
 
 function init(registerMechanism)
-	registerMechanism("PLAIN", {"plain", "plain_test"}, plain);
+	registerMechanism("PLAIN", {"plain", "plain_test", "plain_hashed"}, plain);
 end
 
 return _M;
--- a/util/sasl/scram.lua	Thu May 20 11:32:24 2010 +0100
+++ b/util/sasl/scram.lua	Thu May 20 11:44:41 2010 +0100
@@ -1,5 +1,5 @@
 -- sasl.lua v0.4
--- Copyright (C) 2008-2009 Tobias Markmann
+-- Copyright (C) 2008-2010 Tobias Markmann
 --
 --    All rights reserved.
 --
@@ -28,6 +28,16 @@
 
 --=========================
 --SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10
+
+--[[
+Supported Authentication Backends
+
+scram-{MECH}:
+	function(username, realm)
+		return salted_password, iteration_count, salt, state;
+	end
+]]
+
 local default_i = 4096
 
 local function bp( b )
@@ -82,77 +92,95 @@
 	return username;
 end
 
-local function scram_sha_1(self, message)
-	if not self.state then self["state"] = {} 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 not self.state.name then
-		-- we are processing client_first_message
-		local client_first_message = message;
-		self.state["client_first_message"] = client_first_message;
-		self.state["name"] = client_first_message:match("n=(.+),r=")
-		self.state["clientnonce"] = client_first_message:match("r=([^,]+)")
+		if not self.state.name then
+			-- we are processing client_first_message
+			local client_first_message = message;
+			self.state["client_first_message"] = client_first_message;
+			self.state["name"] = client_first_message:match("n=(.+),r=")
+			self.state["clientnonce"] = client_first_message:match("r=([^,]+)")
 		
-		if not self.state.name or not self.state.clientnonce then
-			return "failure", "malformed-request";
-		end
+			if not self.state.name or not self.state.clientnonce then
+				return "failure", "malformed-request";
+			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();
-		self.state["salt"] = generate_uuid();
+			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
 		
-		local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..default_i;
-		self.state["server_first_message"] = server_first_message;
-		return "challenge", server_first_message
-	else
-		if type(message) ~= "string" then return "failure", "malformed-request" end
-		-- we are processing client_final_message
-		local client_final_message = message;
+			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;
+				self.state.salted_password = Hi(HMAC_f, password, self.state.salt, default_i);
+			elseif self.profile["scram_"..hash_name] then
+				local salted_password, iteration_count, salt, state = self.profile["scram-"..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
 		
-		self.state["proof"] = client_final_message:match("p=(.+)");
-		self.state["nonce"] = client_final_message:match("r=(.+),p=");
-		self.state["channelbinding"] = client_final_message:match("c=(.+),r=");
-		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
+			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
+			if type(message) ~= "string" then return "failure", "malformed-request" end
+			-- we are processing client_final_message
+			local client_final_message = message;
+		
+			self.state["proof"] = client_final_message:match("p=(.+)");
+			self.state["nonce"] = client_final_message:match("r=(.+),p=");
+			self.state["channelbinding"] = client_final_message:match("c=(.+),r=");
+			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
 		
-		local password, state;
-		if self.profile.plain then
-			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."
+			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
-		
-		local SaltedPassword = Hi(hmac_sha1, password, self.state.salt, default_i)
-		local ClientKey = hmac_sha1(SaltedPassword, "Client Key")
-		local ServerKey = hmac_sha1(SaltedPassword, "Server Key")
-		local StoredKey = sha1(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_sha1(StoredKey, AuthMessage)
-		local ClientProof     = binaryXOR(ClientKey, ClientSignature)
-		local ServerSignature = hmac_sha1(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
+	return scram_hash;
 end
 
 function init(registerMechanism)
-	registerMechanism("SCRAM-SHA-1", {"plain"}, scram_sha_1);
+	local function registerSCRAMMechanism(hash_name, hash, hmac_hash)
+		registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hash_name:lower())}, scram_gen(hash_name:lower(), hash, hmac_hash));
+	end
+	
+	registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);
 end
 
 return _M;
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/xmppstream.lua	Thu May 20 11:44:41 2010 +0100
@@ -0,0 +1,168 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- 
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+
+local lxp = require "lxp";
+local st = require "util.stanza";
+
+local tostring = tostring;
+local t_insert = table.insert;
+local t_concat = table.concat;
+
+local default_log = require "util.logger".init("xmlhandlers");
+
+local error = error;
+
+module "xmppstream"
+
+local new_parser = lxp.new;
+
+local ns_prefixes = {
+	["http://www.w3.org/XML/1998/namespace"] = "xml";
+};
+
+local xmlns_streams = "http://etherx.jabber.org/streams";
+
+local ns_separator = "\1";
+local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
+
+function new_sax_handlers(session, stream_callbacks)
+	local xml_handlers = {};
+	
+	local log = session.log or default_log;
+	
+	local cb_streamopened = stream_callbacks.streamopened;
+	local cb_streamclosed = stream_callbacks.streamclosed;
+	local cb_error = stream_callbacks.error or function(session, e) error("XML stream error: "..tostring(e)); end;
+	local cb_handlestanza = stream_callbacks.handlestanza;
+	
+	local stream_ns = stream_callbacks.stream_ns or xmlns_streams;
+	local stream_tag = stream_ns..ns_separator..(stream_callbacks.stream_tag or "stream");
+	local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error");
+	
+	local stream_default_ns = stream_callbacks.default_ns;
+	
+	local chardata, stanza = {};
+	function xml_handlers:StartElement(tagname, attr)
+		if stanza and #chardata > 0 then
+			-- We have some character data in the buffer
+			stanza:text(t_concat(chardata));
+			chardata = {};
+		end
+		local curr_ns,name = tagname:match(ns_pattern);
+		if name == "" then
+			curr_ns, name = "", curr_ns;
+		end
+
+		if curr_ns ~= stream_default_ns then
+			attr.xmlns = curr_ns;
+		end
+		
+		-- FIXME !!!!!
+		for i=1,#attr do
+			local k = attr[i];
+			attr[i] = nil;
+			local ns, nm = k:match(ns_pattern);
+			if nm ~= "" then
+				ns = ns_prefixes[ns]; 
+				if ns then 
+					attr[ns..":"..nm] = attr[k];
+					attr[k] = nil;
+				end
+			end
+		end
+		
+		if not stanza then --if we are not currently inside a stanza
+			if session.notopen then
+				if tagname == stream_tag then
+					if cb_streamopened then
+						cb_streamopened(session, attr);
+					end
+				else
+					-- Garbage before stream?
+					cb_error(session, "no-stream");
+				end
+				return;
+			end
+			if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then
+				cb_error(session, "invalid-top-level-element");
+			end
+			
+			stanza = st.stanza(name, attr);
+		else -- we are inside a stanza, so add a tag
+			attr.xmlns = nil;
+			if curr_ns ~= stream_default_ns then
+				attr.xmlns = curr_ns;
+			end
+			stanza:tag(name, attr);
+		end
+	end
+	function xml_handlers:CharacterData(data)
+		if stanza then
+			t_insert(chardata, data);
+		end
+	end
+	function xml_handlers:EndElement(tagname)
+		if stanza then
+			if #chardata > 0 then
+				-- We have some character data in the buffer
+				stanza:text(t_concat(chardata));
+				chardata = {};
+			end
+			-- Complete stanza
+			if #stanza.last_add == 0 then
+				if tagname ~= stream_error_tag then
+					cb_handlestanza(session, stanza);
+				else
+					cb_error(session, "stream-error", stanza);
+				end
+				stanza = nil;
+			else
+				stanza:up();
+			end
+		else
+			if tagname == stream_tag then
+				if cb_streamclosed then
+					cb_streamclosed(session);
+				end
+			else
+				local curr_ns,name = tagname:match(ns_pattern);
+				if name == "" then
+					curr_ns, name = "", curr_ns;
+				end
+				cb_error(session, "parse-error", "unexpected-element-close", name);
+			end
+			stanza, chardata = nil, {};
+		end
+	end
+	
+	local function reset()
+		stanza, chardata = nil, {};
+	end
+	
+	return xml_handlers, { reset = reset };
+end
+
+function new(session, stream_callbacks)
+	local handlers, meta = new_sax_handlers(session, stream_callbacks);
+	local parser = new_parser(handlers, ns_separator);
+	local parse = parser.parse;
+
+	return {
+		reset = function ()
+			parser = new_parser(handlers, ns_separator);
+			parse = parser.parse;
+			meta.reset();
+		end,
+		feed = function (self, data)
+			return parse(parser, data);
+		end
+	};
+end
+
+return _M;