Changeset

282:80e7de32b618

Merging my new SASL code with Waqas' adjusted saslauth module.
author Tobias Markmann <tm@ayena.de>
date Sat, 15 Nov 2008 13:47:17 +0100
parents 280:516f4c901991 (current diff) 281:826308c07627 (diff)
children 283:8e1fd8ff66ee
files plugins/mod_saslauth.lua
diffstat 14 files changed, 404 insertions(+), 95 deletions(-) [+]
line wrap: on
line diff
--- a/core/componentmanager.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/core/componentmanager.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -25,8 +25,9 @@
 	if not hosts[host] then
 		-- TODO check for host well-formedness
 		components[host] = component;
-		hosts[host] = {type = "component", host = host, connected = true};
+		hosts[host] = {type = "component", host = host, connected = true, s2sout = {} };
 		log("debug", "component added: "..host);
+		return hosts[host];
 	else
 		log("error", "Attempt to set component for existing host: "..host);
 	end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/offlinemanager.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -0,0 +1,32 @@
+
+local datamanager = require "util.datamanager";
+local st = require "util.stanza";
+local datetime = require "util.datetime";
+local ipairs = ipairs;
+
+module "offlinemanager"
+
+function store(node, host, stanza)
+	stanza.attr.stamp = datetime.datetime();
+	stanza.attr.stamp_legacy = datetime.legacy();
+	return datamanager.list_append(node, host, "offline", st.preserialize(stanza));
+end
+
+function load(node, host)
+	local data = datamanager.list_load(node, host, "offline");
+	if not data then return; end
+	for k, v in ipairs(data) do
+		local stanza = st.deserialize(v);
+		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;
+		data[k] = stanza;
+	end
+	return data;
+end
+
+function deleteAll(node, host)
+	return datamanager.list_store(node, host, "offline", nil);
+end
+
+return _M;
--- a/core/rostermanager.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/core/rostermanager.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -1,8 +1,6 @@
 
-local mainlog = log;
-local function log(type, message)
-	mainlog(type, "rostermanager", message);
-end
+
+local log = require "util.logger".init("rostermanager");
 
 local setmetatable = setmetatable;
 local format = string.format;
@@ -234,7 +232,7 @@
 		if item.subscription == "from" then
 			item.subscription = "none";
 			changed = true;
-		elseif item.subscription == both then
+		elseif item.subscription == "both" then
 			item.subscription = "to";
 			changed = true;
 		end
--- a/core/s2smanager.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/core/s2smanager.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -4,12 +4,15 @@
 local socket = require "socket";
 local format = string.format;
 local t_insert = table.insert;
+local get_traceback = debug.traceback;
 local tostring, pairs, ipairs, getmetatable, print, newproxy, error, tonumber
     = tostring, pairs, ipairs, getmetatable, print, newproxy, error, tonumber;
 
 local connlisteners_get = require "net.connlisteners".get;
 local wraptlsclient = require "net.server".wraptlsclient;
 local modulemanager = require "core.modulemanager";
+local st = require "stanza";
+local stanza = st.stanza;
 
 local uuid_gen = require "util.uuid".generate;
 
@@ -21,28 +24,37 @@
 
 local dialback_secret = "This is very secret!!! Ha!";
 
-local srvmap = { ["gmail.com"] = "talk.google.com", ["identi.ca"] = "longlance.controlezvous.ca", ["cdr.se"] = "jabber.cdr.se" };
+local srvmap = { ["gmail.com"] = "talk.google.com", ["identi.ca"] = "hampton.controlezvous.ca", ["cdr.se"] = "jabber.cdr.se" };
 
 module "s2smanager"
 
-function connect_host(from_host, to_host)
-end
-
 function send_to_host(from_host, to_host, data)
-	local host = hosts[to_host];
+	local host = hosts[from_host].s2sout[to_host];
 	if host then
-		-- Write to connection
-		if host.type == "s2sout_unauthed" and not host.notopen and not host.dialback_key then
-			log("debug", "trying to send over unauthed s2sout to "..to_host..", authing it now...");
-			initiate_dialback(host);
-			if not host.sendq then host.sendq = { data };
-			else t_insert(host.sendq, data); end
+		-- We have a connection to this host already
+		if host.type == "s2sout_unauthed" then
+			host.log("debug", "trying to send over unauthed s2sout to "..to_host..", authing it now...");
+			if not host.notopen and not host.dialback_key then
+				host.log("debug", "dialback had not been initiated");
+				initiate_dialback(host);
+			end
+			
+			-- Queue stanza until we are able to send it
+			if host.sendq then t_insert(host.sendq, data);
+			else host.sendq = { data }; end
+		elseif host.type == "local" or host.type == "component" then
+			log("error", "Trying to send a stanza to ourselves??")
+			log("error", "Traceback: %s", get_traceback());
+			log("error", "Stanza: %s", tostring(data));
 		else
-			log("debug", "going to send stanza to "..to_host.." from "..from_host);
+			(host.log or log)("debug", "going to send stanza to "..to_host.." from "..from_host);
 			-- FIXME
-			if hosts[to_host].from_host ~= from_host then log("error", "WARNING! This might, possibly, be a bug, but it might not..."); end
-			hosts[to_host].sends2s(data);
-			log("debug", "stanza sent over "..hosts[to_host].type);
+			if host.from_host ~= from_host then
+				log("error", "WARNING! This might, possibly, be a bug, but it might not...");
+				log("error", "We are going to send from %s instead of %s", host.from_host, from_host);
+			end
+			host.sends2s(data);
+			host.log("debug", "stanza sent over "..host.type);
 		end
 	else
 		log("debug", "opening a new outgoing connection for this stanza");
@@ -52,10 +64,6 @@
 	end
 end
 
-function disconnect_host(host)
-	
-end
-
 local open_sessions = 0;
 
 function new_incoming(conn)
@@ -72,23 +80,27 @@
 
 function new_outgoing(from_host, to_host)
 		local host_session = { to_host = to_host, from_host = from_host, notopen = true, type = "s2sout_unauthed", direction = "outgoing" };
-		hosts[to_host] = host_session;
+		hosts[from_host].s2sout[to_host] = host_session;
 		local cl = connlisteners_get("xmppserver");
 		
 		local conn, handler = socket.tcp()
 		
+		--FIXME: Below parameters (ports/ip) are incorrect (use SRV)
+		to_host = srvmap[to_host] or to_host;
+		
+		conn:settimeout(0);
+		local success, err = conn:connect(to_host, 5269);
+		if not success then
+			log("warn", "s2s connect() failed: %s", err);
+		end
+		
+		conn = wraptlsclient(cl, conn, to_host, 5269, 0, 1, hosts[from_host].ssl_ctx );
+		host_session.conn = conn;
 		
 		-- Register this outgoing connection so that xmppserver_listener knows about it
 		-- otherwise it will assume it is a new incoming connection
 		cl.register_outgoing(conn, host_session);
 		
-		--FIXME: Below parameters (ports/ip) are incorrect (use SRV)
-		to_host = srvmap[to_host] or to_host;
-		conn:settimeout(0.1);
-		conn:connect(to_host, 5269);
-		conn = wraptlsclient(cl, conn, to_host, 5269, 0, 1, hosts[from_host].ssl_ctx );
-		host_session.conn = conn;
-		
 		do
 			local conn_name = "s2sout"..tostring(conn):match("[a-f0-9]*$");
 			host_session.log = logger_init(conn_name);
@@ -103,7 +115,6 @@
 end
 
 function streamopened(session, attr)
-	session.log("debug", "s2s stream opened");
 	local send = session.sends2s;
 	
 	session.version = tonumber(attr.version) or 0;
@@ -124,7 +135,7 @@
 		session.streamid = uuid_gen();
 		print(session, session.from_host, "incoming s2s stream opened");
 		send("<?xml version='1.0'?>");
-		send(format("<stream:stream xmlns='jabber:server' xmlns:db='jabber:server:dialback' xmlns:stream='http://etherx.jabber.org/streams' id='%s' from='%s'>", session.streamid, session.to_host));
+		send(stanza("stream:stream", { xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback', ["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.to_host }):top_tag());
 	elseif session.direction == "outgoing" then
 		-- If we are just using the connection for verifying dialback keys, we won't try and auth it
 		if not attr.id then error("stream response did not give us a streamid!!!"); end
@@ -147,7 +158,7 @@
 	end
 
 	send("</stream:features>");]]
-	log("info", "s2s stream opened successfully");
+
 	session.notopen = nil;
 end
 
@@ -194,7 +205,7 @@
 	
 	if session.direction == "outgoing" then
 		if sendq then
-			session.log("debug", "sending queued stanzas across new outgoing connection to "..session.to_host);
+			session.log("debug", "sending "..#sendq.." queued stanzas across new outgoing connection to "..session.to_host);
 			for i, data in ipairs(sendq) do
 				send(data);
 				sendq[i] = nil;
@@ -207,7 +218,7 @@
 function destroy_session(session)
 	(session.log or log)("info", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
 	if session.direction == "outgoing" then
-		hosts[session.to_host] = nil;
+		hosts[session.from_host].s2sout[session.to_host] = nil;
 	end
 	session.conn = nil;
 	session.disconnect = nil;
--- a/core/stanza_router.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/core/stanza_router.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -13,6 +13,7 @@
 
 local rostermanager = require "core.rostermanager";
 local sessionmanager = require "core.sessionmanager";
+local offlinemanager = require "core.offlinemanager";
 
 local s2s_verify_dialback = require "core.s2smanager".verify_dialback;
 local s2s_make_authenticated = require "core.s2smanager".make_authenticated;
@@ -31,7 +32,7 @@
 local print = print;
 
 function core_process_stanza(origin, stanza)
-	log("debug", "Received["..origin.type.."]: "..tostring(st.reply(st.reply(stanza))))
+	log("debug", "Received[%s]: %s", origin.type, stanza:pretty_top_tag())
 
 	if not stanza.attr.xmlns then stanza.attr.xmlns = "jabber:client"; end -- FIXME Hack. This should be removed when we fix namespace handling.
 	-- TODO verify validity of stanza (as well as JID validity)
@@ -149,6 +150,10 @@
 							core_route_stanza(origin, request);
 						end
 					end
+					for _, msg in ipairs(offlinemanager.load(node, host) or {}) do
+						origin.send(msg); -- FIXME do we need to modify to/from in any way?
+					end
+					offlinemanager.deleteAll(node, host);
 				end
 				origin.priority = 0;
 				if stanza.attr.type == "unavailable" then
@@ -168,11 +173,23 @@
 				end
 				stanza.attr.to = nil; -- reset it
 			else
-				-- TODO error, bad type
+				log("warn", "Unhandled c2s presence: %s", tostring(stanza));
+				if stanza.attr.type ~= "error" then
+					origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- FIXME correct error?
+				end
+			end
+		else
+			log("warn", "Unhandled c2s stanza: %s", tostring(stanza));
+			if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+				origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- FIXME correct error?
 			end
 		end -- TODO handle other stanzas
 	else
-		log("warn", "Unhandled origin: %s", origin.type); -- FIXME reply with error
+		log("warn", "Unhandled origin: %s", origin.type);
+		if stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then
+			-- s2s stanzas can get here
+			(origin.sends2s or origin.send)(st.error_reply(stanza, "cancel", "service-unavailable")); -- FIXME correct error?
+		end
 	end
 end
 
@@ -328,8 +345,14 @@
 							t_insert(recipients, session);
 						end
 					end
+					local count = 0;
 					for _, session in pairs(recipients) do
 						session.send(stanza);
+						count = count + 1;
+					end
+					if count == 0 then
+						offlinemanager.store(node, host, stanza);
+						-- TODO deal with storage errors
 					end
 				else
 					-- TODO send IQ error
@@ -349,7 +372,12 @@
 						-- TODO send unavailable presence or unsubscribed
 					end
 				elseif stanza.name == "message" then
-					-- TODO send message error, or store offline messages
+					if stanza.attr.type == "chat" or stanza.attr.type == "normal" or not stanza.attr.type then
+						offlinemanager.store(node, host, stanza);
+						-- FIXME don't store messages with only chat state notifications
+					end
+					-- TODO allow configuration of offline storage
+					-- TODO send error if not storing offline
 				elseif stanza.name == "iq" then
 					-- TODO send IQ error
 				end
--- a/main.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/main.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -17,7 +17,7 @@
 
 if config.hosts and #config.hosts > 0 then
 	for _, host in pairs(config.hosts) do
-		hosts[host] = {type = "local", connected = true, sessions = {}, host = host};
+		hosts[host] = {type = "local", connected = true, sessions = {}, host = host, s2sout = {} };
 	end
 else error("No hosts defined in the configuration file"); end
 
@@ -32,6 +32,9 @@
 require "core.sessionmanager"
 require "core.stanza_router"
 
+pcall(require, "remdebug.engine");
+if remdebug then remdebug.engine.start() end
+
 local start = require "net.connlisteners".start;
 require "util.stanza"
 require "util.jid"
--- a/net/xmppclient_listener.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/net/xmppclient_listener.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -78,7 +78,7 @@
 	if session then
 		if session.last_presence and session.last_presence.attr.type ~= "unavailable" then
 			local pres = st.presence{ type = "unavailable" };
-			if err == "closed" then err = "connection closed"; end
+			if err == "closed" then err = "connection closed"; end --FIXME where did err come from?
 			pres:tag("status"):text("Disconnected: "..err);
 			session.stanza_dispatch(pres);
 		end
--- a/plugins/mod_dialback.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/plugins/mod_dialback.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -22,6 +22,7 @@
 			type = "invalid"
 			log("warn", "Asked to verify a dialback key that was incorrect. An imposter is claiming to be %s?", attr.to);
 		end
+		log("debug", "verifyied dialback key... it is %s", type);
 		origin.sends2s(format("<db:verify from='%s' to='%s' id='%s' type='%s'>%s</db:verify>", attr.to, attr.from, attr.id, type, stanza[1]));
 	end);
 
@@ -38,7 +39,7 @@
 		send_s2s(origin.to_host, origin.from_host,
 			format("<db:verify from='%s' to='%s' id='%s'>%s</db:verify>", origin.to_host, origin.from_host,
 				origin.streamid, origin.dialback_key));
-		hosts[origin.from_host].dialback_verifying = origin;
+		hosts[origin.to_host].s2sout[origin.from_host].dialback_verifying = origin;
 	end);
 
 add_handler({ "s2sout_unauthed", "s2sout" }, "verify", xmlns_dialback,
--- a/plugins/mod_saslauth.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/plugins/mod_saslauth.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -2,6 +2,7 @@
 local st = require "util.stanza";
 local send = require "core.sessionmanager".send_to_session;
 local sm_bind_resource = require "core.sessionmanager".bind_resource;
+local jid
 
 local usermanager_validate_credentials = require "core.usermanager".validate_credentials;
 local t_concat, t_insert = table.concat, table.insert;
@@ -15,48 +16,100 @@
 
 local new_sasl = require "util.sasl".new;
 
-add_handler("c2s_unauthed", "auth", xmlns_sasl, function (session, stanza)
-	if not session.sasl_handler then
-		session.sasl_handler = new_sasl(stanza.attr.mechanism, 
-			function (username, password)
-				-- onAuth
-				require "core.usermanager"
-				if usermanager_validate_credentials(session.host, username, password) then
-					return true;
-				end
-				return false;
-			end,
-			function (username)
-				-- onSuccess
-				local success, err = sessionmanager.make_authenticated(session, username);
-				if not success then
-					sessionmanager.destroy_session(session);
-					return;
-				end
-				session.sasl_handler = nil;
-				session:reset_stream();
-			end,
-			function (reason)
-				-- onFail
-				log("debug", "SASL failure, reason: %s", reason);
-			end,
-			function (stanza)
-				-- onWrite
-				log("debug", "SASL writes: %s", tostring(stanza));
-				send(session, stanza);
+local function build_reply(status, ret)
+	local reply = st.stanza(status, {xmlns = xmlns_sasl});
+	if status == "challenge" then
+		reply:text(ret or "");
+	elseif status == "failure" then
+		reply:tag(ret):up();
+	elseif status == "success" then
+		reply:text(ret or "");
+	else
+		error("Unknown sasl status: "..status);
+	end
+	return reply;
+end
+
+local function handle_status(session, status)
+	if status == "failure" then
+		session.sasl_handler = nil;
+	elseif status == "success" then
+		session.sasl_handler = nil;
+		session:reset_stream();
+	end
+end
+
+local function password_callback(jid, mechanism)
+	local node, host = jid_split(jid);
+	local password = (datamanager.load(node, host, "accounts") or {}).password; -- FIXME handle hashed passwords
+	local func = function(x) return x; end;
+	if password then
+		if mechanism == "PLAIN" then
+			return func, password;
+		elseif mechanism == "DIGEST-MD5" then
+			return func, require "hashes".md5(node.."::"..password);
+		end
+	end
+	return func, nil;
+end
+
+add_handler("c2s_unauthed", "auth", xmlns_sasl,
+		function (session, stanza)
+			if not session.sasl_handler then
+				session.sasl_handler = new_sasl(stanza.attr.mechanism, session.host, password_callback);
+				local status, ret = session.sasl_handler:feed(stanza[1]);
+				handle_status(session, status);
+				session.send(build_reply(status, ret));
+				--[[session.sasl_handler = new_sasl(stanza.attr.mechanism, 
+					function (username, password)
+						-- onAuth
+						require "core.usermanager"
+						if usermanager_validate_credentials(session.host, username, password) then
+							return true;
+						end
+						return false;
+					end,
+					function (username)
+						-- onSuccess
+						local success, err = sessionmanager.make_authenticated(session, username);
+						if not success then
+							sessionmanager.destroy_session(session);
+							return;
+						end
+						session.sasl_handler = nil;
+						session:reset_stream();
+					end,
+					function (reason)
+						-- onFail
+						log("debug", "SASL failure, reason: %s", reason);
+					end,
+					function (stanza)
+						-- onWrite
+						log("debug", "SASL writes: %s", tostring(stanza));
+						send(session, stanza);
+					end
+				);
+				session.sasl_handler:feed(stanza);	]]
+			else
+				error("Client tried to negotiate SASL again", 0);
 			end
-		);
-		session.sasl_handler:feed(stanza);	
-	else
-		error("Client tried to negotiate SASL again", 0);
-	end	
-end);
+		end);
 
-add_handler("c2s_unauthed", "response", xmlns_sasl, function (session, stanza)
-	if session.sasl_handler then
-		session.sasl_handler:feed(stanza);	
-	end	
-end);
+add_handler("c2s_unauthed", "abort", xmlns_sasl,
+	function(session, stanza)
+		if not session.sasl_handler then error("Attempt to abort when sasl has not started"); end
+		local status, ret = session.sasl_handler:feed(stanza[1]);
+		handle_status(session, status);
+		session.send(build_reply(status, ret));
+	end);
+
+add_handler("c2s_unauthed", "response", xmlns_sasl,
+	function(session, stanza)
+		if not session.sasl_handler then error("Attempt to respond when sasl has not started"); end
+		local status, ret = session.sasl_handler:feed(stanza[1]);
+		handle_status(session, status);
+		session.send(build_reply(status, ret));
+	end);
 		
 add_event_hook("stream-features", 
 					function (session, features)												
--- a/util/datamanager.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/util/datamanager.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -1,14 +1,15 @@
 local format = string.format;
 local setmetatable, type = setmetatable, type;
-local pairs = pairs;
+local pairs, ipairs = pairs, ipairs;
 local char = string.char;
 local loadfile, setfenv, pcall = loadfile, setfenv, pcall;
 local log = log;
 local io_open = io.open;
 local os_remove = os.remove;
-local tostring = tostring;
+local tostring, tonumber = tostring, tonumber;
 local error = error;
 local next = next;
+local t_insert = table.insert;
 
 local indent = function(f, i)
 	for n = 1, i do
@@ -69,13 +70,14 @@
 
 ------- API -------------
 
-function getpath(username, host, datastore)
+function getpath(username, host, datastore, ext)
+	ext = ext or "dat";
 	if username then
-		return format("data/%s/%s/%s.dat", encode(host), datastore, encode(username));
+		return format("data/%s/%s/%s.%s", encode(host), datastore, encode(username), ext);
 	elseif host then
-		return format("data/%s/%s.dat", encode(host), datastore);
+		return format("data/%s/%s.%s", encode(host), datastore, ext);
 	else
-		return format("data/%s.dat", datastore);
+		return format("data/%s.%s", datastore, ext);
 	end
 end
 
@@ -115,4 +117,59 @@
 	return true;
 end
 
-return _M;
\ No newline at end of file
+function list_append(username, host, datastore, data)
+	if not data then return; end
+	-- save the datastore
+	local f, msg = io_open(getpath(username, host, datastore, "list"), "a+");
+	if not f then
+		log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil"));
+		return;
+	end
+	f:write("item(");
+	simplesave(f, data, 1);
+	f:write(");\n");
+	f:close();
+	return true;
+end
+
+function list_store(username, host, datastore, data)
+	if not data then
+		data = {};
+	end
+	-- save the datastore
+	local f, msg = io_open(getpath(username, host, datastore, "list"), "w+");
+	if not f then
+		log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or "nil").."@"..(host or "nil"));
+		return;
+	end
+	for _, d in ipairs(data) do
+		f:write("item(");
+		simplesave(f, d, 1);
+		f:write(");\n");
+	end
+	f:close();
+	if not next(data) then -- try to delete empty datastore
+		os_remove(getpath(username, host, datastore, "list"));
+	end
+	-- we write data even when we are deleting because lua doesn't have a
+	-- platform independent way of checking for non-exisitng files
+	return true;
+end
+
+function list_load(username, host, datastore)
+	local data, ret = loadfile(getpath(username, host, datastore, "list"));
+	if not data then
+		log("warn", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or "nil").."@"..(host or "nil"));
+		return nil;
+	end
+	local items = {};
+	setfenv(data, {item = function(i) t_insert(items, i); end});
+	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;
+	end
+	return items;
+end
+
+return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/datetime.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -0,0 +1,28 @@
+-- XEP-0082: XMPP Date and Time Profiles
+
+local os_date = os.date;
+local error = error;
+
+module "datetime"
+
+function date()
+	return os_date("!%Y-%m-%d");
+end
+
+function datetime()
+	return os_date("!%Y-%m-%dT%H:%M:%SZ");
+end
+
+function time()
+	return os_date("!%H:%M:%S");
+end
+
+function legacy()
+	return os_date("!%Y%m%dT%H:%M:%S");
+end
+
+function parse(s)
+	error("datetime.parse: Not implemented"); -- TODO
+end
+
+return _M;
--- a/util/logger.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/util/logger.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -3,8 +3,21 @@
 local print = print;
 local debug = debug;
 local tostring = tostring;
+
+local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring;
+local do_pretty_printing = not os.getenv("WINDIR");
+
 module "logger"
 
+local logstyles = {};
+
+--TODO: This should be done in config, but we don't have proper config yet
+if do_pretty_printing then
+	logstyles["info"] = getstyle("bold");
+	logstyles["warn"] = getstyle("bold", "yellow");
+	logstyles["error"] = getstyle("bold", "red");
+end
+
 function init(name)
 	--name = nil; -- While this line is not commented, will automatically fill in file/line number info
 	return 	function (level, message, ...)
@@ -13,9 +26,9 @@
 					level = level .. ","..tostring(inf.short_src):match("[^/]*$")..":"..inf.currentline;
 				end
 				if ... then 
-					print(level, format(message, ...));
+					print(name, getstring(logstyles[level], level), format(message, ...));
 				else
-					print(level, message);
+					print(name, getstring(logstyles[level], level), message);
 				end
 			end
 end
--- a/util/stanza.lua	Wed Nov 12 21:38:46 2008 +0100
+++ b/util/stanza.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -10,8 +10,11 @@
 local print          =          print;
 local unpack        =        unpack;
 local s_gsub        =   string.gsub;
+local os = os;
 
-local debug = debug;
+local do_pretty_printing = not os.getenv("WINDIR");
+local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring;
+
 local log = require "util.logger".init("stanza");
 
 module "stanza"
@@ -105,6 +108,14 @@
 	return s_format("<%s%s>%s</%s>", t.name, attr_string, children_text, t.name);
 end
 
+function stanza_mt.top_tag(t)
+	local attr_string = "";
+	if t.attr then
+		for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(" %s='%s'", k, tostring(v)); end end
+	end
+	return s_format("<%s%s>", t.name, attr_string);
+end
+
 function stanza_mt.__add(s1, s2)
 	return s1:add_direct_child(s2);
 end
@@ -184,4 +195,44 @@
 	return stanza("presence", attr);
 end
 
+if do_pretty_printing then
+	local style_attrk = getstyle("yellow");
+	local style_attrv = getstyle("red");
+	local style_tagname = getstyle("red");
+	local style_punc = getstyle("magenta");
+	
+	local attr_format = " "..getstring(style_attrk, "%s")..getstring(style_punc, "=")..getstring(style_attrv, "'%s'");
+	local top_tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">");
+	--local tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">").."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
+	local tag_format = top_tag_format.."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
+	function stanza_mt.pretty_print(t)
+		local children_text = "";
+		for n, child in ipairs(t) do
+			if type(child) == "string" then	
+				children_text = children_text .. xml_escape(child);
+			else
+				children_text = children_text .. child:pretty_print();
+			end
+		end
+
+		local attr_string = "";
+		if t.attr then
+			for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(attr_format, k, tostring(v)); end end
+		end
+		return s_format(tag_format, t.name, attr_string, children_text, t.name);
+	end
+	
+	function stanza_mt.pretty_top_tag(t)
+		local attr_string = "";
+		if t.attr then
+			for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(attr_format, k, tostring(v)); end end
+		end
+		return s_format(top_tag_format, t.name, attr_string);
+	end
+else
+	-- Sorry, fresh out of colours for you guys ;)
+	stanza_mt.pretty_print = stanza_mt.__tostring;
+	stanza_mt.pretty_top_tag = stanza_mt.top_tag;
+end
+
 return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/termcolours.lua	Sat Nov 15 13:47:17 2008 +0100
@@ -0,0 +1,33 @@
+local t_concat, t_insert = table.concat, table.insert;
+local char, format = string.char, string.format;
+local ipairs = ipairs;
+module "termcolours"
+
+local stylemap = {
+			reset = 0; bright = 1, dim = 2, underscore = 4, blink = 5, reverse = 7, hidden = 8;
+			black = 30; red = 31; green = 32; yellow = 33; blue = 34; magenta = 35; cyan = 36; white = 37;
+			["black background"] = 40; ["red background"] = 41; ["green background"] = 42; ["yellow background"] = 43; ["blue background"] = 44; ["magenta background"] = 45; ["cyan background"] = 46; ["white background"] = 47;
+			bold = 1, dark = 2, underline = 4, underlined = 4, normal = 0;
+		}
+
+local fmt_string = char(0x1B).."[%sm%s"..char(0x1B).."[0m";
+function getstring(style, text)
+	if style then
+		return format(fmt_string, style, text);
+	else
+		return text;
+	end
+end
+
+function getstyle(...)
+	local styles, result = { ... }, {};
+	for i, style in ipairs(styles) do
+		style = stylemap[style];
+		if style then
+			t_insert(result, style);
+		end
+	end
+	return t_concat(result, ";");
+end
+
+return _M;