Changeset

4321:4dcdba6900f2

Merge 0.8->trunk
author Matthew Wild <mwild1@gmail.com>
date Sun, 12 Jun 2011 22:21:10 +0100
parents 4319:b46b766ce0af (diff) 4320:c3f39ccc7c50 (current diff)
children 4322:aff627b1ce95
files
diffstat 21 files changed, 1430 insertions(+), 157 deletions(-) [+]
line wrap: on
line diff
--- a/certs/openssl.cnf	Sun Jun 12 20:35:53 2011 +0100
+++ b/certs/openssl.cnf	Sun Jun 12 22:21:10 2011 +0100
@@ -43,10 +43,10 @@
 # See http://tools.ietf.org/html/draft-ietf-xmpp-3920bis#section-13.7.1.2 for more info.
 
 DNS.0       =                                           example.com
-otherName.0 =                             xmppAddr;UTF8:example.com
+otherName.0 =                 xmppAddr;FORMAT:UTF8,UTF8:example.com
 otherName.1 =            SRVName;IA5STRING:_xmpp-client.example.com
 otherName.2 =            SRVName;IA5STRING:_xmpp-server.example.com
 
 DNS.1       =                                conference.example.com
-otherName.3 =                  xmppAddr;UTF8:conference.example.com
+otherName.3 =      xmppAddr;FORMAT:UTF8,UTF8:conference.example.com
 otherName.4 = SRVName;IA5STRING:_xmpp-server.conference.example.com
--- a/core/certmanager.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/core/certmanager.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -22,6 +22,8 @@
 -- Global SSL options if not overridden per-host
 local default_ssl_config = configmanager.get("*", "core", "ssl");
 local default_capath = "/etc/ssl/certs";
+local default_verify = (ssl and ssl.x509 and { "peer", "client_once", "continue", "ignore_purpose" }) or "none";
+local default_options = { "no_sslv2" };
 
 function create_context(host, mode, user_ssl_config)
 	user_ssl_config = user_ssl_config or default_ssl_config;
@@ -37,8 +39,8 @@
 		certificate = resolve_path(config_path, user_ssl_config.certificate);
 		capath = resolve_path(config_path, user_ssl_config.capath or default_capath);
 		cafile = resolve_path(config_path, user_ssl_config.cafile);
-		verify = user_ssl_config.verify or "none";
-		options = user_ssl_config.options or "no_sslv2";
+		verify = user_ssl_config.verify or default_verify;
+		options = user_ssl_config.options or default_options;
 		ciphers = user_ssl_config.ciphers;
 		depth = user_ssl_config.depth;
 	};
--- a/core/s2smanager.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/core/s2smanager.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -27,6 +27,7 @@
 local st = require "stanza";
 local stanza = st.stanza;
 local nameprep = require "util.encodings".stringprep.nameprep;
+local cert_verify_identity = require "util.x509".verify_identity;
 
 local fire_event = prosody.events.fire_event;
 local uuid_gen = require "util.uuid".generate;
@@ -392,23 +393,54 @@
 		from=from, to=to, version='1.0', ["xml:lang"]='en'}):top_tag());
 end
 
+local function check_cert_status(session)
+	local conn = session.conn:socket()
+	local cert
+	if conn.getpeercertificate then
+		cert = conn:getpeercertificate()
+	end
+
+	if cert then
+		local chain_valid, err = conn:getpeerchainvalid()
+		if not chain_valid then
+			session.cert_chain_status = "invalid";
+			(session.log or log)("debug", "certificate chain validation result: %s", err);
+		else
+			session.cert_chain_status = "valid";
+
+			local host = session.direction == "incoming" and session.from_host or session.to_host
+
+			-- We'll go ahead and verify the asserted identity if the
+			-- connecting server specified one.
+			if host then
+				if cert_verify_identity(host, "xmpp-server", cert) then
+					session.cert_identity_status = "valid"
+				else
+					session.cert_identity_status = "invalid"
+				end
+			end
+		end
+	end
+end
+
 function streamopened(session, attr)
 	local send = session.sends2s;
 	
 	-- TODO: #29: SASL/TLS on s2s streams
 	session.version = tonumber(attr.version) or 0;
 	
+	-- TODO: Rename session.secure to session.encrypted
 	if session.secure == false then
 		session.secure = true;
 	end
-	
+
 	if session.direction == "incoming" then
 		-- Send a reply stream header
 		session.to_host = attr.to and nameprep(attr.to);
 		session.from_host = attr.from and nameprep(attr.from);
 	
 		session.streamid = uuid_gen();
-		(session.log or log)("debug", "incoming s2s received <stream:stream>");
+		(session.log or log)("debug", "Incoming s2s received <stream:stream>");
 		if session.to_host then
 			if not hosts[session.to_host] then
 				-- Attempting to connect to a host we don't serve
@@ -426,6 +458,9 @@
 				return;
 			end
 		end
+
+		if session.secure and not session.cert_chain_status then check_cert_status(session); end
+
 		send("<?xml version='1.0'?>");
 		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, to=session.from_host, version=(session.version > 0 and "1.0" or nil) }):top_tag());
@@ -445,7 +480,9 @@
 		-- 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
 		session.streamid = attr.id;
-	
+
+		if session.secure and not session.cert_chain_status then check_cert_status(session); end
+
 		-- Send unauthed buffer
 		-- (stanzas which are fine to send before dialback)
 		-- Note that this is *not* the stanza queue (which
@@ -588,7 +625,7 @@
 
 function destroy_session(session, reason)
 	if session.destroyed then return; end
-	(session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host));
+	(session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or ""));
 	
 	if session.direction == "outgoing" then
 		hosts[session.from_host].s2sout[session.to_host] = nil;
--- a/core/sessionmanager.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/core/sessionmanager.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -103,7 +103,7 @@
 end
 
 function destroy_session(session, err)
-	(session.log or log)("info", "Destroying session for %s (%s@%s)", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)");
+	(session.log or log)("info", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or "");
 	if session.destroyed then return; end
 	
 	-- Remove session/resource from user's session list
--- a/net/dns.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/net/dns.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -389,6 +389,14 @@
 	rr.a = string.format('%i.%i.%i.%i', b1, b2, b3, b4);
 end
 
+function resolver:AAAA(rr)
+	local addr = {};
+	for i = 1, rr.rdlength, 2 do
+		local b1, b2 = self:byte(2);
+		table.insert(addr, ("%02x%02x"):format(b1, b2));
+	end
+	rr.aaaa = table.concat(addr, ":");
+end
 
 function resolver:CNAME(rr)    -- - - - - - - - - - - - - - - - - - - -  CNAME
 	rr.cname = self:name();
@@ -479,7 +487,7 @@
 end
 
 function resolver:TXT(rr)    -- - - - - - - - - - - - - - - - - - - - - -  TXT
-	rr.txt = self:sub (rr.rdlength);
+	rr.txt = self:sub (self:byte());
 end
 
 
--- a/net/server_select.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/net/server_select.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -939,6 +939,7 @@
 	
 	loop = loop,
 	link = link,
+	step = step,
 	stats = stats,
 	closeall = closeall,
 	addtimer = addtimer,
--- a/plugins/adhoc/mod_adhoc.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/adhoc/mod_adhoc.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -1,5 +1,5 @@
 -- Copyright (C) 2009 Thilo Cestonaro
--- Copyright (C) 2009-2010 Florian Zeitz
+-- Copyright (C) 2009-2011 Florian Zeitz
 --
 -- This file is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
@@ -51,12 +51,14 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type == "get" and stanza.tags[1].attr.node
 	    and stanza.tags[1].attr.node == xmlns_cmd then
-		local privileged = is_admin(stanza.attr.from, stanza.attr.to);
+		local admin = is_admin(stanza.attr.from, stanza.attr.to);
+		local global_admin = is_admin(stanza.attr.from);
 		reply = st.reply(stanza);
 		reply:tag("query", { xmlns = xmlns_disco.."#items",
 		    node = xmlns_cmd });
 		for node, command in pairs(commands) do
-			if (command.permission == "admin" and privileged)
+			if (command.permission == "admin" and admin)
+			    or (command.permission == "global_admin" and global_admin)
 			    or (command.permission == "user") then
 				reply:tag("item", { name = command.name,
 				    node = node, jid = module:get_host() });
@@ -73,9 +75,10 @@
 	if stanza.attr.type == "set" then
 		local node = stanza.tags[1].attr.node
 		if commands[node] then
-			local privileged = is_admin(stanza.attr.from, stanza.attr.to);
-			if commands[node].permission == "admin"
-			    and not privileged then
+			local admin = is_admin(stanza.attr.from, stanza.attr.to);
+			local global_admin = is_admin(stanza.attr.from);
+			if (commands[node].permission == "admin" and not admin)
+			    or (commands[node].permission == "global_admin" and not global_admin) then
 				origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up()
 				    :add_child(commands[node]:cmdtag("canceled")
 					:tag("note", {type="error"}):text("You don't have permission to execute this command")));
--- a/plugins/mod_admin_adhoc.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_admin_adhoc.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -1,4 +1,4 @@
--- Copyright (C) 2009-2010 Florian Zeitz
+-- Copyright (C) 2009-2011 Florian Zeitz
 --
 -- This file is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
@@ -101,6 +101,16 @@
 	end
 end
 
+function config_reload_handler(self, data, state)
+	local ok, err = prosody.reload_config();
+	if ok then
+		return { status = "completed", info = "Configuration reloaded (modules may need to be reloaded for this to have an effect)" };
+	else
+		return { status = "completed", error = { message = "Failed to reload config: " .. tostring(err) } };
+	end
+end
+
+
 function delete_user_command_handler(self, data, state)
 	local delete_user_layout = dataforms_new{
 		title = "Deleting a User";
@@ -540,8 +550,6 @@
 	else
 		return { status = "executing", form = shut_down_service_layout }, "executing";
 	end
-
-	return true;
 end
 
 function unload_modules_handler(self, data, state)
@@ -582,6 +590,7 @@
 
 local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin");
 local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin");
+local config_reload_desc = adhoc_new("Reload configuration", "http://prosody.im/protocol/config#reload", config_reload_handler, "global_admin");
 local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin");
 local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin");
 local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org/protocol/admin#get-user-password", get_user_password_handler, "admin");
@@ -591,11 +600,12 @@
 local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin");
 local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin");
 local reload_modules_desc = adhoc_new("Reload modules", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin");
-local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "admin");
+local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "global_admin");
 local unload_modules_desc = adhoc_new("Unload modules", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin");
 
 module:add_item("adhoc", add_user_desc);
 module:add_item("adhoc", change_user_password_desc);
+module:add_item("adhoc", config_reload_desc);
 module:add_item("adhoc", delete_user_desc);
 module:add_item("adhoc", end_user_session_desc);
 module:add_item("adhoc", get_user_password_desc);
--- a/plugins/mod_admin_telnet.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_admin_telnet.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -19,6 +19,7 @@
 require "util.iterators";
 local jid_bare = require "util.jid".bare;
 local set, array = require "util.set", require "util.array";
+local cert_verify_identity = require "util.x509".verify_identity;
 
 local commands = {};
 local def_env = {};
@@ -498,7 +499,7 @@
 		for remotehost, session in pairs(host_session.s2sout) do
 			if (not match_jid) or remotehost:match(match_jid) or host:match(match_jid) then
 				count_out = count_out + 1;
-				print("    "..host.." -> "..remotehost..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
+				print("    "..host.." -> "..remotehost..(session.cert_identity_status == "valid" and " (secure)" or "")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
 				if session.sendq then
 					print("        There are "..#session.sendq.." queued outgoing stanzas for this connection");
 				end
@@ -535,7 +536,7 @@
 				-- Pft! is what I say to list comprehensions
 				or (session.hosts and #array.collect(keys(session.hosts)):filter(subhost_filter)>0)) then
 				count_in = count_in + 1;
-				print("    "..host.." <- "..(session.from_host or "(unknown)")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
+				print("    "..host.." <- "..(session.from_host or "(unknown)")..(session.cert_identity_status == "valid" and " (secure)" or "")..(session.secure and " (encrypted)" or "")..(session.compressed and " (compressed)" or ""));
 				if session.type == "s2sin_unauthed" then
 						print("        Connection not yet authenticated");
 				end
@@ -561,6 +562,109 @@
 	return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
 end
 
+local function print_subject(print, subject)
+	for _, entry in ipairs(subject) do
+		print(
+			("    %s: %q"):format(
+				entry.name or entry.oid,
+				entry.value:gsub("[\r\n%z%c]", " ")
+			)
+		);
+	end
+end
+
+function def_env.s2s:showcert(domain)
+	local ser = require "util.serialization".serialize;
+	local print = self.session.print;
+	local domain_sessions = set.new(array.collect(keys(incoming_s2s)))
+		/function(session) return session.from_host == domain; end;
+	for local_host in values(prosody.hosts) do
+		local s2sout = local_host.s2sout;
+		if s2sout and s2sout[domain] then
+			domain_sessions:add(s2sout[domain]);
+		end
+	end
+	local cert_set = {};
+	for session in domain_sessions do
+		local conn = session.conn;
+		conn = conn and conn:socket();
+		if not conn.getpeercertificate then
+			if conn.dohandshake then
+				error("This version of LuaSec does not support certificate viewing");
+			end
+		else
+			local cert = conn:getpeercertificate();
+			if cert then
+				local digest = cert:digest("sha1");
+				if not cert_set[digest] then
+					local chain_valid, chain_err = conn:getpeerchainvalid();
+					cert_set[digest] = {
+						{
+						  from = session.from_host,
+						  to = session.to_host,
+						  direction = session.direction
+						};
+						chain_valid = chain_valid;
+						chain_err = chain_err;
+						cert = cert;
+					};
+				else
+					table.insert(cert_set[digest], {
+						from = session.from_host,
+						to = session.to_host,
+						direction = session.direction
+					});
+				end
+			end
+		end
+	end
+	local domain_certs = array.collect(values(cert_set));
+	-- Phew. We now have a array of unique certificates presented by domain.
+	local print = self.session.print;
+	local n_certs = #domain_certs;
+	
+	if n_certs == 0 then
+		return "No certificates found for "..domain;
+	end
+	
+	local function _capitalize_and_colon(byte)
+		return string.upper(byte)..":";
+	end
+	local function pretty_fingerprint(hash)
+		return hash:gsub("..", _capitalize_and_colon):sub(1, -2);
+	end
+	
+	for cert_info in values(domain_certs) do
+		local cert = cert_info.cert;
+		print("---")
+		print("Fingerprint (SHA1): "..pretty_fingerprint(cert:digest("sha1")));
+		print("");
+		local n_streams = #cert_info;
+		print("Currently used on "..n_streams.." stream"..(n_streams==1 and "" or "s")..":");
+		for _, stream in ipairs(cert_info) do
+			if stream.direction == "incoming" then
+				print("    "..stream.to.." <- "..stream.from);
+			else
+				print("    "..stream.from.." -> "..stream.to);
+			end
+		end
+		print("");
+		local chain_valid, err = cert_info.chain_valid, cert_info.chain_err;
+		local valid_identity = cert_verify_identity(domain, "xmpp-server", cert);
+		print("Trusted certificate: "..(chain_valid and "Yes" or ("No ("..err..")")));
+		print("Issuer: ");
+		print_subject(print, cert:issuer());
+		print("");
+		print("Valid for "..domain..": "..(valid_identity and "Yes" or "No"));
+		print("Subject:");
+		print_subject(print, cert:subject());
+	end
+	print("---");
+	return ("Showing "..n_certs.." certificate"
+		..(n_certs==1 and "" or "s")
+		.." presented by "..domain..".");
+end
+
 function def_env.s2s:close(from, to)
 	local print, count = self.session.print, 0;
 	
--- a/plugins/mod_bosh.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_bosh.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -162,7 +162,13 @@
 			end
 		end
 		
-		return true; -- Inform httpserver we shall reply later
+		if session.bosh_terminate then
+			session.log("debug", "Closing session with %d requests open", #session.requests);
+			session:close();
+			return nil;
+		else
+			return true; -- Inform httpserver we shall reply later
+		end
 	end
 end
 
@@ -202,7 +208,6 @@
 
 	local session_close_response = { headers = default_headers, body = tostring(close_reply) };
 
-	--FIXME: Quite sure we shouldn't reply to all requests with the error
 	for _, held_request in ipairs(session.requests) do
 		held_request:send(session_close_response);
 		held_request:destroy();
@@ -255,7 +260,13 @@
 			local oldest_request = r[1];
 			if oldest_request then
 				log("debug", "We have an open request, so sending on that");
-				response.body = t_concat{"<body xmlns='http://jabber.org/protocol/httpbind' sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>", tostring(s), "</body>" };
+				response.body = t_concat({
+					"<body xmlns='http://jabber.org/protocol/httpbind' ",
+					session.bosh_terminate and "type='terminate' " or "",
+					"sid='", sid, "' xmlns:stream = 'http://etherx.jabber.org/streams'>",
+					tostring(s),
+					"</body>"
+				});
 				oldest_request:send(response);
 				--log("debug", "Sent");
 				if oldest_request.stayopen then
@@ -327,13 +338,6 @@
 		session.rid = rid;
 	end
 	
-	if attr.type == "terminate" then
-		-- Client wants to end this session
-		session:close();
-		request.notopen = nil;
-		return;
-	end
-	
 	if session.notopen then
 		local features = st.stanza("stream:features");
 		hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
@@ -342,6 +346,12 @@
 		session.notopen = nil;
 	end
 	
+	if attr.type == "terminate" then
+		-- Client wants to end this session, which we'll do
+		-- after processing any stanzas in this request
+		session.bosh_terminate = true;
+	end
+
 	request.notopen = nil; -- Signals that we accept this opening tag
 	t_insert(session.requests, request);
 	request.sid = sid;
--- a/plugins/mod_component.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_component.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -86,7 +86,7 @@
 		send = session.send;
 		main_session = session;
 		session.on_destroy = on_destroy;
-		session.component_validate_from = module:get_option_boolean("validate_from_addresses") ~= false;
+		session.component_validate_from = module:get_option_boolean("validate_from_addresses", true);
 		log("info", "Component successfully authenticated: %s", session.host);
 		session.send(st.stanza("handshake"));
 	else -- TODO: Implement stanza distribution
--- a/plugins/mod_dialback.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_dialback.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -131,12 +131,22 @@
 	end
 end);
 
+module:hook_stanza("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza)
+	if origin.external_auth == "failed" then
+		module:log("debug", "SASL EXTERNAL failed, falling back to dialback");
+		s2s_initiate_dialback(origin);
+		return true;
+	end
+end, 100);
+
 module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
-	s2s_initiate_dialback(origin);
-	return true;
+	if not origin.external_auth or origin.external_auth == "failed" then
+		s2s_initiate_dialback(origin);
+		return true;
+	end
 end, 100);
 
 -- Offer dialback to incoming hosts
 module:hook("s2s-stream-features", function (data)
-	data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):tag("optional"):up():up();
+	data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):up();
 end);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_pubsub.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -0,0 +1,364 @@
+local pubsub = require "util.pubsub";
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local uuid_generate = require "util.uuid".generate;
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
+local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
+
+local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
+local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
+local pubsub_disco_name = module:get_option("name");
+if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end
+
+local service;
+
+local handlers = {};
+
+function handle_pubsub_iq(event)
+	local origin, stanza = event.origin, event.stanza;
+	local pubsub = stanza.tags[1];
+	local action = pubsub.tags[1];
+	local handler = handlers[stanza.attr.type.."_"..action.name];
+	if handler then
+		handler(origin, stanza, action);
+		return true;
+	end
+end
+
+local pubsub_errors = {
+	["conflict"] = { "cancel", "conflict" };
+	["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
+	["item-not-found"] = { "cancel", "item-not-found" };
+	["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
+	["forbidden"] = { "cancel", "forbidden" };
+};
+function pubsub_error_reply(stanza, error)
+	local e = pubsub_errors[error];
+	local reply = st.error_reply(stanza, unpack(e, 1, 3));
+	if e[4] then
+		reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
+	end
+	return reply;
+end
+
+function handlers.get_items(origin, stanza, items)
+	local node = items.attr.node;
+	local item = items:get_child("item");
+	local id = item and item.attr.id;
+	
+	local ok, results = service:get_items(node, stanza.attr.from, id);
+	if not ok then
+		return origin.send(pubsub_error_reply(stanza, results));
+	end
+	
+	local data = st.stanza("items", { node = node });
+	for _, entry in pairs(results) do
+		data:add_child(entry);
+	end
+	if data then
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:add_child(data);
+	else
+		reply = pubsub_error_reply(stanza, "item-not-found");
+	end
+	return origin.send(reply);
+end
+
+function handlers.get_subscriptions(origin, stanza, subscriptions)
+	local node = subscriptions.attr.node;
+	local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
+	if not ok then
+		return origin.send(pubsub_error_reply(stanza, ret));
+	end
+	local reply = st.reply(stanza)
+		:tag("pubsub", { xmlns = xmlns_pubsub })
+			:tag("subscriptions");
+	for _, sub in ipairs(ret) do
+		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
+	end
+	return origin.send(reply);
+end
+
+function handlers.set_create(origin, stanza, create)
+	local node = create.attr.node;
+	local ok, ret, reply;
+	if node then
+		ok, ret = service:create(node, stanza.attr.from);
+		if ok then
+			reply = st.reply(stanza);
+		else
+			reply = pubsub_error_reply(stanza, ret);
+		end
+	else
+		repeat
+			node = uuid_generate();
+			ok, ret = service:create(node, stanza.attr.from);
+		until ok or ret ~= "conflict";
+		if ok then
+			reply = st.reply(stanza)
+				:tag("pubsub", { xmlns = xmlns_pubsub })
+					:tag("create", { node = node });
+		else
+			reply = pubsub_error_reply(stanza, ret);
+		end
+	end
+	return origin.send(reply);
+end
+
+function handlers.set_subscribe(origin, stanza, subscribe)
+	local node, jid = subscribe.attr.node, subscribe.attr.jid;
+	local ok, ret = service:add_subscription(node, stanza.attr.from, jid);
+	local reply;
+	if ok then
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:tag("subscription", {
+					node = node,
+					jid = jid,
+					subscription = "subscribed"
+				});
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	return origin.send(reply);
+end
+
+function handlers.set_unsubscribe(origin, stanza, unsubscribe)
+	local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
+	local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
+	local reply;
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	return origin.send(reply);
+end
+
+function handlers.set_publish(origin, stanza, publish)
+	local node = publish.attr.node;
+	local item = publish:get_child("item");
+	local id = (item and item.attr.id) or uuid_generate();
+	local ok, ret = service:publish(node, stanza.attr.from, id, item);
+	local reply;
+	if ok then
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:tag("publish", { node = node })
+					:tag("item", { id = id });
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	return origin.send(reply);
+end
+
+function handlers.set_retract(origin, stanza, retract)
+	local node, notify = retract.attr.node, retract.attr.notify;
+	notify = (notify == "1") or (notify == "true");
+	local item = retract:get_child("item");
+	local id = item and item.attr.id
+	local reply, notifier;
+	if notify then
+		notifier = st.stanza("retract", { id = id });
+	end
+	local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	return origin.send(reply);
+end
+
+function simple_broadcast(node, jids, item)
+	item = st.clone(item);
+	item.attr.xmlns = nil; -- Clear the pubsub namespace
+	local message = st.message({ from = module.host, type = "headline" })
+		:tag("event", { xmlns = xmlns_pubsub_event })
+			:tag("items", { node = node })
+				:add_child(item);
+	for jid in pairs(jids) do
+		module:log("debug", "Sending notification to %s", jid);
+		message.attr.to = jid;
+		core_post_stanza(hosts[module.host], message);
+	end
+end
+
+module:hook("iq/host/http://jabber.org/protocol/pubsub:pubsub", handle_pubsub_iq);
+
+local disco_info;
+
+local feature_map = {
+	create = { "create-nodes", autocreate_on_publish and "instant-nodes", "item-ids" };
+	retract = { "delete-items", "retract-items" };
+	publish = { "publish" };
+	get_items = { "retrieve-items" };
+	add_subscription = { "subscribe" };
+	get_subscriptions = { "retrieve-subscriptions" };
+};
+
+local function add_disco_features_from_service(disco, service)
+	for method, features in pairs(feature_map) do
+		if service[method] then
+			for _, feature in ipairs(features) do
+				if feature then
+					disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
+				end
+			end
+		end
+	end
+	for affiliation in pairs(service.config.capabilities) do
+		if affiliation ~= "none" and affiliation ~= "owner" then
+			disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up();
+		end
+	end
+end
+
+local function build_disco_info(service)
+	local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
+		:tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up()
+		:tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
+	add_disco_features_from_service(disco_info, service);
+	return disco_info;
+end
+
+module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	local node = stanza.tags[1].attr.node;
+	if not node then
+		return origin.send(st.reply(stanza):add_child(disco_info));
+	else
+		local ok, ret = service:get_nodes(stanza.attr.from);
+		if ok and not ret[node] then
+			ok, ret = false, "item-not-found";
+		end
+		if not ok then
+			return origin.send(pubsub_error_reply(stanza, ret));
+		end
+		local reply = st.reply(stanza)
+			:tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node })
+				:tag("identity", { category = "pubsub", type = "leaf" });
+		return origin.send(reply);
+	end
+end);
+
+local function handle_disco_items_on_node(event)
+	local stanza, origin = event.stanza, event.origin;
+	local query = stanza.tags[1];
+	local node = query.attr.node;
+	local ok, ret = service:get_items(node, stanza.attr.from);
+	if not ok then
+		return origin.send(pubsub_error_reply(stanza, ret));
+	end
+	
+	local reply = st.reply(stanza)
+		:tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
+	
+	for id, item in pairs(ret) do
+		reply:tag("item", { jid = module.host, name = id }):up();
+	end
+	
+	return origin.send(reply);
+end
+
+
+module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
+	if event.stanza.tags[1].attr.node then
+		return handle_disco_items_on_node(event);
+	end
+	local ok, ret = service:get_nodes(event.stanza.attr.from);
+	if not ok then
+		event.origin.send(pubsub_error_reply(stanza, ret));
+	else
+		local reply = st.reply(event.stanza)
+			:tag("query", { xmlns = "http://jabber.org/protocol/disco#items" });
+		for node, node_obj in pairs(ret) do
+			reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
+		end
+		event.origin.send(reply);
+	end
+	return true;
+end);
+
+local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
+local function get_affiliation(jid)
+	local bare_jid = jid_bare(jid);
+	if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
+		return admin_aff;
+	end
+end
+
+function set_service(new_service)
+	service = new_service;
+	module.environment.service = service;
+	disco_info = build_disco_info(service);
+end
+
+function module.save()
+	return { service = service };
+end
+
+function module.restore(data)
+	set_service(data.service);
+end
+
+set_service(pubsub.new({
+	capabilities = {
+		none = {
+			create = false;
+			publish = false;
+			retract = false;
+			get_nodes = true;
+			
+			subscribe = true;
+			unsubscribe = true;
+			get_subscription = true;
+			get_subscriptions = true;
+			get_items = true;
+			
+			subscribe_other = false;
+			unsubscribe_other = false;
+			get_subscription_other = false;
+			get_subscriptions_other = false;
+			
+			be_subscribed = true;
+			be_unsubscribed = true;
+			
+			set_affiliation = false;
+		};
+		owner = {
+			create = true;
+			publish = true;
+			retract = true;
+			get_nodes = true;
+			
+			subscribe = true;
+			unsubscribe = true;
+			get_subscription = true;
+			get_subscriptions = true;
+			get_items = true;
+			
+			
+			subscribe_other = true;
+			unsubscribe_other = true;
+			get_subscription_other = true;
+			get_subscriptions_other = true;
+			
+			be_subscribed = true;
+			be_unsubscribed = true;
+			
+			set_affiliation = true;
+		};
+	};
+	
+	autocreate_on_publish = autocreate_on_publish;
+	autocreate_on_subscribe = autocreate_on_subscribe;
+	
+	broadcaster = simple_broadcast;
+	get_affiliation = get_affiliation;
+	
+	normalize_jid = jid_bare;
+}));
\ No newline at end of file
--- a/plugins/mod_register.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_register.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -19,9 +19,22 @@
 local jid_bare = require "util.jid".bare;
 
 local compat = module:get_option_boolean("registration_compat", true);
+local allow_registration = module:get_option_boolean("allow_registration", false);
 
 module:add_feature("jabber:iq:register");
 
+local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up();
+module:hook("stream-features", function(event)
+        local session, features = event.origin, event.features;
+
+	-- Advertise registration to unauthorized clients only.
+	if not(allow_registration) or session.type ~= "c2s_unauthed" then
+		return
+	end
+
+	features:add_child(register_stream_feature);
+end);
+
 local function handle_registration_stanza(event)
 	local session, stanza = event.origin, event.stanza;
 
@@ -114,7 +127,7 @@
 module:hook("stanza/iq/jabber:iq:register:query", function(event)
 	local session, stanza = event.origin, event.stanza;
 
-	if module:get_option("allow_registration") == false or session.type ~= "c2s_unauthed" then
+	if not(allow_registration) or session.type ~= "c2s_unauthed" then
 		session.send(st.error_reply(stanza, "cancel", "service-unavailable"));
 	else
 		local query = stanza.tags[1];
--- a/plugins/mod_roster.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_roster.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -22,7 +22,7 @@
 
 module:add_feature("jabber:iq:roster");
 
-local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"}):tag("optional"):up();
+local rosterver_stream_feature = st.stanza("ver", {xmlns="urn:xmpp:features:rosterver"});
 module:hook("stream-features", function(event)
 	local origin, features = event.origin, event.features;
 	if origin.username then
--- a/plugins/mod_saslauth.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/mod_saslauth.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -11,8 +11,11 @@
 local st = require "util.stanza";
 local sm_bind_resource = require "core.sessionmanager".bind_resource;
 local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
+local s2s_make_authenticated = require "core.s2smanager".make_authenticated;
 local base64 = require "util.encodings".base64;
 
+local cert_verify_identity = require "util.x509".verify_identity;
+
 local nodeprep = require "util.encodings".stringprep.nodeprep;
 local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
 local tostring = tostring;
@@ -81,8 +84,123 @@
 	return true;
 end
 
+module:hook_stanza(xmlns_sasl, "success", function (session, stanza)
+	if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+	module:log("debug", "SASL EXTERNAL with %s succeeded", session.to_host);
+	session.external_auth = "succeeded"
+	session:reset_stream();
+
+	local default_stream_attr = {xmlns = "jabber:server", ["xmlns:stream"] = "http://etherx.jabber.org/streams",
+	                            ["xmlns:db"] = 'jabber:server:dialback', version = "1.0", to = session.to_host, from = session.from_host};
+	session.sends2s("<?xml version='1.0'?>");
+	session.sends2s(st.stanza("stream:stream", default_stream_attr):top_tag());
+
+	s2s_make_authenticated(session, session.to_host);
+	return true;
+end)
+
+module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+	if session.type ~= "s2sout_unauthed" or session.external_auth ~= "attempting" then return; end
+
+	module:log("info", "SASL EXTERNAL with %s failed", session.to_host)
+	-- TODO: Log the failure reason
+	session.external_auth = "failed"
+end, 500)
+
+module:hook_stanza(xmlns_sasl, "failure", function (session, stanza)
+	-- TODO: Dialback wasn't loaded.  Do something useful.
+end, 90)
+
+module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+	if session.type ~= "s2sout_unauthed" or not session.secure then return; end
+
+	local mechanisms = stanza:get_child("mechanisms", xmlns_sasl)
+	if mechanisms then
+		for mech in mechanisms:childtags() do
+			if mech[1] == "EXTERNAL" then
+				module:log("debug", "Initiating SASL EXTERNAL with %s", session.to_host);
+				local reply = st.stanza("auth", {xmlns = xmlns_sasl, mechanism = "EXTERNAL"});
+				reply:text(base64.encode(session.from_host))
+				session.sends2s(reply)
+				session.external_auth = "attempting"
+				return true
+			end
+		end
+	end
+end, 150);
+
+local function s2s_external_auth(session, stanza)
+	local mechanism = stanza.attr.mechanism;
+
+	if not session.secure then
+		if mechanism == "EXTERNAL" then
+			session.sends2s(build_reply("failure", "encryption-required"))
+		else
+			session.sends2s(build_reply("failure", "invalid-mechanism"))
+		end
+		return true;
+	end
+
+	if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
+		session.sends2s(build_reply("failure", "invalid-mechanism"))
+		return true;
+	end
+
+	local text = stanza[1]
+	if not text then
+		session.sends2s(build_reply("failure", "malformed-request"))
+		return true
+	end
+
+	-- Either the value is "=" and we've already verified the external
+	-- cert identity, or the value is a string and either matches the
+	-- from_host (
+
+	text = base64.decode(text)
+	if not text then
+		session.sends2s(build_reply("failure", "incorrect-encoding"))
+		return true;
+	end
+
+	if session.cert_identity_status == "valid" then
+		if text ~= "" and text ~= session.from_host then
+			session.sends2s(build_reply("failure", "invalid-authzid"))
+			return true
+		end
+	else
+		if text == "" then
+			session.sends2s(build_reply("failure", "invalid-authzid"))
+			return true
+		end
+
+		local cert = session.conn:socket():getpeercertificate()
+		if (cert_verify_identity(text, "xmpp-server", cert)) then
+			session.cert_identity_status = "valid"
+		else
+			session.cert_identity_status = "invalid"
+			session.sends2s(build_reply("failure", "invalid-authzid"))
+			return true
+		end
+	end
+
+	session.external_auth = "succeeded"
+
+	if not session.from_host then
+		session.from_host = text;
+	end
+	session.sends2s(build_reply("success"))
+	module:log("info", "Accepting SASL EXTERNAL identity from %s", text or session.from_host);
+	s2s_make_authenticated(session, text or session.from_host)
+	session:reset_stream();
+	return true
+end
+
 module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
 	local session, stanza = event.origin, event.stanza;
+	if session.type == "s2sin_unauthed" then
+		return s2s_external_auth(session, stanza)
+	end
+
 	if session.type ~= "c2s_unauthed" then return; end
 
 	if session.sasl_handler and session.sasl_handler.selected then
@@ -141,6 +259,20 @@
 	end
 end);
 
+module:hook("s2s-stream-features", function(event)
+	local origin, features = event.origin, event.features;
+	if origin.secure and origin.type == "s2sin_unauthed" then
+		-- Offer EXTERNAL if chain is valid and either we didn't validate
+		-- the identity or it passed.
+		if origin.cert_chain_status == "valid" and origin.cert_identity_status ~= "invalid" then --TODO: Configurable
+			module:log("debug", "Offering SASL EXTERNAL")
+			features:tag("mechanisms", { xmlns = xmlns_sasl })
+				:tag("mechanism"):text("EXTERNAL")
+			:up():up();
+		end
+	end
+end);
+
 module:hook("iq/self/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
 	local origin, stanza = event.origin, event.stanza;
 	local resource;
--- a/plugins/muc/muc.lib.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/plugins/muc/muc.lib.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -213,6 +213,7 @@
 end
 
 function room_mt:get_disco_info(stanza)
+	local count = 0; for _ in pairs(self._occupants) do count = count + 1; end
 	return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
 		:tag("identity", {category="conference", type="text", name=self:get_name()}):up()
 		:tag("feature", {var="http://jabber.org/protocol/muc"}):up()
@@ -224,7 +225,8 @@
 		:tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up()
 		:add_child(dataform.new({
 			{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
-			{ name = "muc#roominfo_description", label = "Description"}
+			{ name = "muc#roominfo_description", label = "Description"},
+			{ name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) }
 		}):form({["muc#roominfo_description"] = self:get_description()}, 'result'))
 	;
 end
--- a/util-src/encodings.c	Sun Jun 12 20:35:53 2011 +0100
+++ b/util-src/encodings.c	Sun Jun 12 22:21:10 2011 +0100
@@ -117,7 +117,87 @@
 };
 
 /***************** STRINGPREP *****************/
-#ifndef USE_STRINGPREP_ICU
+#ifdef USE_STRINGPREP_ICU
+
+#include <unicode/usprep.h>
+#include <unicode/ustring.h>
+#include <unicode/utrace.h>
+
+static int icu_stringprep_prep(lua_State *L, const UStringPrepProfile *profile)
+{
+	size_t input_len;
+	int32_t unprepped_len, prepped_len, output_len;
+	const char *input;
+	char output[1024];
+
+	UChar unprepped[1024]; /* Temporary unicode buffer (1024 characters) */
+	UChar prepped[1024];
+	
+	UErrorCode err = U_ZERO_ERROR;
+
+	if(!lua_isstring(L, 1)) {
+		lua_pushnil(L);
+		return 1;
+	}
+	input = lua_tolstring(L, 1, &input_len);
+	if (input_len >= 1024) {
+		lua_pushnil(L);
+		return 1;
+	}
+	u_strFromUTF8(unprepped, 1024, &unprepped_len, input, input_len, &err);
+	if (U_FAILURE(err)) {
+		lua_pushnil(L);
+		return 1;
+	}
+	prepped_len = usprep_prepare(profile, unprepped, unprepped_len, prepped, 1024, 0, NULL, &err);
+	if (U_FAILURE(err)) {
+		lua_pushnil(L);
+		return 1;
+	} else {
+		u_strToUTF8(output, 1024, &output_len, prepped, prepped_len, &err);
+		if (U_SUCCESS(err) && output_len < 1024)
+			lua_pushlstring(L, output, output_len);
+		else
+			lua_pushnil(L);
+		return 1;
+	}
+}
+
+UStringPrepProfile *icu_nameprep;
+UStringPrepProfile *icu_nodeprep;
+UStringPrepProfile *icu_resourceprep; 
+UStringPrepProfile *icu_saslprep;
+
+/* initialize global ICU stringprep profiles */
+void init_icu()
+{
+	UErrorCode err = U_ZERO_ERROR;
+	utrace_setLevel(UTRACE_VERBOSE);
+	icu_nameprep = usprep_openByType(USPREP_RFC3491_NAMEPREP, &err);
+	icu_nodeprep = usprep_openByType(USPREP_RFC3920_NODEPREP, &err);
+	icu_resourceprep = usprep_openByType(USPREP_RFC3920_RESOURCEPREP, &err);
+	icu_saslprep = usprep_openByType(USPREP_RFC4013_SASLPREP, &err);
+	if (U_FAILURE(err)) fprintf(stderr, "[c] util.encodings: error: %s\n", u_errorName((UErrorCode)err));
+}
+
+#define MAKE_PREP_FUNC(myFunc, prep) \
+static int myFunc(lua_State *L) { return icu_stringprep_prep(L, prep); }
+
+MAKE_PREP_FUNC(Lstringprep_nameprep, icu_nameprep)		/** stringprep.nameprep(s) */
+MAKE_PREP_FUNC(Lstringprep_nodeprep, icu_nodeprep)		/** stringprep.nodeprep(s) */
+MAKE_PREP_FUNC(Lstringprep_resourceprep, icu_resourceprep)		/** stringprep.resourceprep(s) */
+MAKE_PREP_FUNC(Lstringprep_saslprep, icu_saslprep)		/** stringprep.saslprep(s) */
+
+static const luaL_Reg Reg_stringprep[] =
+{
+	{ "nameprep",	Lstringprep_nameprep	},
+	{ "nodeprep",	Lstringprep_nodeprep	},
+	{ "resourceprep",	Lstringprep_resourceprep	},
+	{ "saslprep",	Lstringprep_saslprep	},
+	{ NULL,		NULL	}
+};
+#else /* USE_STRINGPREP_ICU */
+
 /****************** libidn ********************/
 
 #include <stringprep.h>
@@ -164,41 +244,36 @@
 	{ "saslprep",	Lstringprep_saslprep	},
 	{ NULL,		NULL	}
 };
-
-#else
-#include <unicode/usprep.h>
-#include <unicode/ustring.h>
-#include <unicode/utrace.h>
+#endif
 
-static int icu_stringprep_prep(lua_State *L, const UStringPrepProfile *profile)
+/***************** IDNA *****************/
+#ifdef USE_STRINGPREP_ICU
+#include <unicode/ustdio.h>
+#include <unicode/uidna.h>
+/* IDNA2003 or IDNA2008 ? ? ? */
+static int Lidna_to_ascii(lua_State *L)		/** idna.to_ascii(s) */
 {
-	size_t input_len;
-	int32_t unprepped_len, prepped_len, output_len;
-	const char *input;
+	size_t len;
+	int32_t ulen, dest_len, output_len;
+	const char *s = luaL_checklstring(L, 1, &len);
+	UChar ustr[1024];
+	UErrorCode err = U_ZERO_ERROR;
+	UChar dest[1024];
 	char output[1024];
 
-	UChar unprepped[1024]; /* Temporary unicode buffer (1024 characters) */
-	UChar prepped[1024];
-	
-	UErrorCode err = U_ZERO_ERROR;
-
-	if(!lua_isstring(L, 1)) {
+	u_strFromUTF8(ustr, 1024, &ulen, s, len, &err);
+	if (U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	}
-	input = lua_tolstring(L, 1, &input_len);
-	if (input_len >= 1024) {
-		lua_pushnil(L);
-		return 1;
-	}
-	u_strFromUTF8(unprepped, 1024, &unprepped_len, input, input_len, &err);
-	prepped_len = usprep_prepare(profile, unprepped, unprepped_len, prepped, 1024, 0, NULL, &err);
+
+	dest_len = uidna_IDNToASCII(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err);
 	if (U_FAILURE(err)) {
 		lua_pushnil(L);
 		return 1;
 	} else {
-		u_strToUTF8(output, 1024, &output_len, prepped, prepped_len, &err);
-		if(output_len < 1024)
+		u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err);
+		if (U_SUCCESS(err) && output_len < 1024)
 			lua_pushlstring(L, output, output_len);
 		else
 			lua_pushnil(L);
@@ -206,43 +281,37 @@
 	}
 }
 
-UStringPrepProfile *icu_nameprep;
-UStringPrepProfile *icu_nodeprep;
-UStringPrepProfile *icu_resourceprep; 
-UStringPrepProfile *icu_saslprep;
-
-/* initialize global ICU stringprep profiles */
-void init_icu()
+static int Lidna_to_unicode(lua_State *L)		/** idna.to_unicode(s) */
 {
+	size_t len;
+	int32_t ulen, dest_len, output_len;
+	const char *s = luaL_checklstring(L, 1, &len);
+	UChar ustr[1024];
 	UErrorCode err = U_ZERO_ERROR;
-	utrace_setLevel(UTRACE_VERBOSE);
-	icu_nameprep = usprep_openByType(USPREP_RFC3491_NAMEPREP, &err);
-	icu_nodeprep = usprep_openByType(USPREP_RFC3920_NODEPREP, &err);
-	icu_resourceprep = usprep_openByType(USPREP_RFC3920_RESOURCEPREP, &err);
-	icu_saslprep = usprep_openByType(USPREP_RFC4013_SASLPREP, &err);
-	if (U_FAILURE(err)) fprintf(stderr, "[c] util.encodings: error: %s\n", u_errorName((UErrorCode)err));
+	UChar dest[1024];
+	char output[1024];
+
+	u_strFromUTF8(ustr, 1024, &ulen, s, len, &err);
+	if (U_FAILURE(err)) {
+		lua_pushnil(L);
+		return 1;
+	}
+
+	dest_len = uidna_IDNToUnicode(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err);
+	if (U_FAILURE(err)) {
+		lua_pushnil(L);
+		return 1;
+	} else {
+		u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err);
+		if (U_SUCCESS(err) && output_len < 1024)
+			lua_pushlstring(L, output, output_len);
+		else
+			lua_pushnil(L);
+		return 1;
+	}
 }
 
-#define MAKE_PREP_FUNC(myFunc, prep) \
-static int myFunc(lua_State *L) { return icu_stringprep_prep(L, prep); }
-
-MAKE_PREP_FUNC(Lstringprep_nameprep, icu_nameprep)		/** stringprep.nameprep(s) */
-MAKE_PREP_FUNC(Lstringprep_nodeprep, icu_nodeprep)		/** stringprep.nodeprep(s) */
-MAKE_PREP_FUNC(Lstringprep_resourceprep, icu_resourceprep)		/** stringprep.resourceprep(s) */
-MAKE_PREP_FUNC(Lstringprep_saslprep, icu_saslprep)		/** stringprep.saslprep(s) */
-
-static const luaL_Reg Reg_stringprep[] =
-{
-	{ "nameprep",	Lstringprep_nameprep	},
-	{ "nodeprep",	Lstringprep_nodeprep	},
-	{ "resourceprep",	Lstringprep_resourceprep	},
-	{ "saslprep",	Lstringprep_saslprep	},
-	{ NULL,		NULL	}
-};
-#endif
-
-/***************** IDNA *****************/
-#ifndef USE_STRINGPREP_ICU
+#else /* USE_STRINGPREP_ICU */
 /****************** libidn ********************/
 
 #include <idna.h>
@@ -281,59 +350,6 @@
 		return 1; /* TODO return error message */
 	}
 }
-#else
-#include <unicode/ustdio.h>
-#include <unicode/uidna.h>
-/* IDNA2003 or IDNA2008 ? ? ? */
-static int Lidna_to_ascii(lua_State *L)		/** idna.to_ascii(s) */
-{
-	size_t len;
-	int32_t ulen, dest_len, output_len;
-	const char *s = luaL_checklstring(L, 1, &len);
-	UChar ustr[1024];
-	UErrorCode err = U_ZERO_ERROR;
-	UChar dest[1024];
-	char output[1024];
-
-	u_strFromUTF8(ustr, 1024, &ulen, s, len, &err);
-	dest_len = uidna_IDNToASCII(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err);
-	if (U_FAILURE(err)) {
-		lua_pushnil(L);
-		return 1;
-	} else {
-		u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err);
-		if(output_len < 1024)
-			lua_pushlstring(L, output, output_len);
-		else
-			lua_pushnil(L);
-		return 1;
-	}
-}
-
-static int Lidna_to_unicode(lua_State *L)		/** idna.to_unicode(s) */
-{
-	size_t len;
-	int32_t ulen, dest_len, output_len;
-	const char *s = luaL_checklstring(L, 1, &len);
-	UChar* ustr;
-	UErrorCode err = U_ZERO_ERROR;
-	UChar dest[1024];
-	char output[1024];
-
-	u_strFromUTF8(ustr, 1024, &ulen, s, len, &err);
-	dest_len = uidna_IDNToUnicode(ustr, ulen, dest, 1024, UIDNA_USE_STD3_RULES, NULL, &err);
-	if (U_FAILURE(err)) {
-		lua_pushnil(L);
-		return 1;
-	} else {
-		u_strToUTF8(output, 1024, &output_len, dest, dest_len, &err);
-		if(output_len < 1024)
-			lua_pushlstring(L, output, output_len);
-		else
-			lua_pushnil(L);
-		return 1;
-	}
-}
 #endif
 
 static const luaL_Reg Reg_idna[] =
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/pubsub.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -0,0 +1,341 @@
+module("pubsub", package.seeall);
+
+local service = {};
+local service_mt = { __index = service };
+
+local default_config = {
+	broadcaster = function () end;
+	get_affiliation = function () end;
+	capabilities = {};
+};
+
+function new(config)
+	config = config or {};
+	return setmetatable({
+		config = setmetatable(config, { __index = default_config });
+		affiliations = {};
+		subscriptions = {};
+		nodes = {};
+	}, service_mt);
+end
+
+function service:jids_equal(jid1, jid2)
+	local normalize = self.config.normalize_jid;
+	return normalize(jid1) == normalize(jid2);
+end
+
+function service:may(node, actor, action)
+	if actor == true then return true; end
+	
+	local node_obj = self.nodes[node];
+	local node_aff = node_obj and node_obj.affiliations[actor];
+	local service_aff = self.affiliations[actor]
+	                 or self.config.get_affiliation(actor, node, action)
+	                 or "none";
+	
+	-- Check if node allows/forbids it
+	local node_capabilities = node_obj and node_obj.capabilities;
+	if node_capabilities then
+		local caps = node_capabilities[node_aff or service_aff];
+		if caps then
+			local can = caps[action];
+			if can ~= nil then
+				return can;
+			end
+		end
+	end
+	
+	-- Check service-wide capabilities instead
+	local service_capabilities = self.config.capabilities;
+	local caps = service_capabilities[node_aff or service_aff];
+	if caps then
+		local can = caps[action];
+		if can ~= nil then
+			return can;
+		end
+	end
+	
+	return false;
+end
+
+function service:set_affiliation(node, actor, jid, affiliation)
+	-- Access checking
+	if not self:may(node, actor, "set_affiliation") then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		return false, "item-not-found";
+	end
+	node_obj.affiliations[jid] = affiliation;
+	local _, jid_sub = self:get_subscription(node, true, jid);
+	if not jid_sub and not self:may(node, jid, "be_unsubscribed") then
+		local ok, err = self:add_subscription(node, true, jid);
+		if not ok then
+			return ok, err;
+		end
+	elseif jid_sub and not self:may(node, jid, "be_subscribed") then
+		local ok, err = self:add_subscription(node, true, jid);
+		if not ok then
+			return ok, err;
+		end
+	end
+	return true;
+end
+
+function service:add_subscription(node, actor, jid, options)
+	-- Access checking
+	local cap;
+	if actor == true or jid == actor or self:jids_equal(actor, jid) then
+		cap = "subscribe";
+	else
+		cap = "subscribe_other";
+	end
+	if not self:may(node, actor, cap) then
+		return false, "forbidden";
+	end
+	if not self:may(node, jid, "be_subscribed") then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		if not self.config.autocreate_on_subscribe then
+			return false, "item-not-found";
+		else
+			local ok, err = self:create(node, actor);
+			if not ok then
+				return ok, err;
+			end
+			node_obj = self.nodes[node];
+		end
+	end
+	node_obj.subscribers[jid] = options or true;
+	local normal_jid = self.config.normalize_jid(jid);
+	local subs = self.subscriptions[normal_jid];
+	if subs then
+		if not subs[jid] then
+			subs[jid] = { [node] = true };
+		else
+			subs[jid][node] = true;
+		end
+	else
+		self.subscriptions[normal_jid] = { [jid] = { [node] = true } };
+	end
+	return true;
+end
+
+function service:remove_subscription(node, actor, jid)
+	-- Access checking
+	local cap;
+	if actor == true or jid == actor or self:jids_equal(actor, jid) then
+		cap = "unsubscribe";
+	else
+		cap = "unsubscribe_other";
+	end
+	if not self:may(node, actor, cap) then
+		return false, "forbidden";
+	end
+	if not self:may(node, jid, "be_unsubscribed") then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		return false, "item-not-found";
+	end
+	if not node_obj.subscribers[jid] then
+		return false, "not-subscribed";
+	end
+	node_obj.subscribers[jid] = nil;
+	local normal_jid = self.config.normalize_jid(jid);
+	local subs = self.subscriptions[normal_jid];
+	if subs then
+		local jid_subs = subs[jid];
+		if jid_subs then
+			jid_subs[node] = nil;
+			if next(jid_subs) == nil then
+				subs[jid] = nil;
+			end
+		end
+		if next(subs) == nil then
+			self.subscriptions[normal_jid] = nil;
+		end
+	end
+	return true;
+end
+
+function service:get_subscription(node, actor, jid)
+	-- Access checking
+	local cap;
+	if actor == true or jid == actor or self:jids_equal(actor, jid) then
+		cap = "get_subscription";
+	else
+		cap = "get_subscription_other";
+	end
+	if not self:may(node, actor, cap) then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		return false, "item-not-found";
+	end
+	return true, node_obj.subscribers[jid];
+end
+
+function service:create(node, actor)
+	-- Access checking
+	if not self:may(node, actor, "create") then
+		return false, "forbidden";
+	end
+	--
+	if self.nodes[node] then
+		return false, "conflict";
+	end
+	
+	self.nodes[node] = {
+		name = node;
+		subscribers = {};
+		config = {};
+		data = {};
+		affiliations = {};
+	};
+	local ok, err = self:set_affiliation(node, true, actor, "owner");
+	if not ok then
+		self.nodes[node] = nil;
+	end
+	return ok, err;
+end
+
+function service:publish(node, actor, id, item)
+	-- Access checking
+	if not self:may(node, actor, "publish") then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		if not self.config.autocreate_on_publish then
+			return false, "item-not-found";
+		end
+		local ok, err = self:create(node, actor);
+		if not ok then
+			return ok, err;
+		end
+		node_obj = self.nodes[node];
+	end
+	node_obj.data[id] = item;
+	self.config.broadcaster(node, node_obj.subscribers, item);
+	return true;
+end
+
+function service:retract(node, actor, id, retract)
+	-- Access checking
+	if not self:may(node, actor, "retract") then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if (not node_obj) or (not node_obj.data[id]) then
+		return false, "item-not-found";
+	end
+	node_obj.data[id] = nil;
+	if retract then
+		self.config.broadcaster(node, node_obj.subscribers, retract);
+	end
+	return true
+end
+
+function service:get_items(node, actor, id)
+	-- Access checking
+	if not self:may(node, actor, "get_items") then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		return false, "item-not-found";
+	end
+	if id then -- Restrict results to a single specific item
+		return true, { [id] = node_obj.data[id] };
+	else
+		return true, node_obj.data;
+	end
+end
+
+function service:get_nodes(actor)
+	-- Access checking
+	if not self:may(nil, actor, "get_nodes") then
+		return false, "forbidden";
+	end
+	--
+	return true, self.nodes;
+end
+
+function service:get_subscriptions(node, actor, jid)
+	-- Access checking
+	local cap;
+	if actor == true or jid == actor or self:jids_equal(actor, jid) then
+		cap = "get_subscriptions";
+	else
+		cap = "get_subscriptions_other";
+	end
+	if not self:may(node, actor, cap) then
+		return false, "forbidden";
+	end
+	--
+	local node_obj;
+	if node then
+		node_obj = self.nodes[node];
+		if not node_obj then
+			return false, "item-not-found";
+		end
+	end
+	local normal_jid = self.config.normalize_jid(jid);
+	local subs = self.subscriptions[normal_jid];
+	-- We return the subscription object from the node to save
+	-- a get_subscription() call for each node.
+	local ret = {};
+	if subs then
+		for jid, subscribed_nodes in pairs(subs) do
+			if node then -- Return only subscriptions to this node
+				if subscribed_nodes[node] then
+					ret[#ret+1] = {
+						node = subscribed_node;
+						jid = jid;
+						subscription = node_obj.subscribers[jid];
+					};
+				end
+			else -- Return subscriptions to all nodes
+				local nodes = self.nodes;
+				for subscribed_node in pairs(subscribed_nodes) do
+					ret[#ret+1] = {
+						node = subscribed_node;
+						jid = jid;
+						subscription = nodes[subscribed_node].subscribers[jid];
+					};
+				end
+			end
+		end
+	end
+	return true, ret;
+end
+
+-- Access models only affect 'none' affiliation caps, service/default access level...
+function service:set_node_capabilities(node, actor, capabilities)
+	-- Access checking
+	if not self:may(node, actor, "configure") then
+		return false, "forbidden";
+	end
+	--
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		return false, "item-not-found";
+	end
+	node_obj.capabilities = capabilities;
+	return true;
+end
+
+return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/x509.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -0,0 +1,211 @@
+-- Prosody IM
+-- Copyright (C) 2010 Matthew Wild
+-- Copyright (C) 2010 Paul Aurich
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+-- TODO: I feel a fair amount of this logic should be integrated into Luasec,
+-- so that everyone isn't re-inventing the wheel.  Dependencies on
+-- IDN libraries complicate that.
+
+
+-- [TLS-CERTS] - http://tools.ietf.org/html/draft-saintandre-tls-server-id-check-10
+-- [XMPP-CORE] - http://tools.ietf.org/html/draft-ietf-xmpp-3920bis-18
+-- [SRV-ID]    - http://tools.ietf.org/html/rfc4985
+-- [IDNA]      - http://tools.ietf.org/html/rfc5890
+-- [LDAP]      - http://tools.ietf.org/html/rfc4519
+-- [PKIX]      - http://tools.ietf.org/html/rfc5280
+
+local nameprep = require "util.encodings".stringprep.nameprep;
+local idna_to_ascii = require "util.encodings".idna.to_ascii;
+local log = require "util.logger".init("x509");
+
+module "x509"
+
+local oid_commonname = "2.5.4.3"; -- [LDAP] 2.3
+local oid_subjectaltname = "2.5.29.17"; -- [PKIX] 4.2.1.6
+local oid_xmppaddr = "1.3.6.1.5.5.7.8.5"; -- [XMPP-CORE]
+local oid_dnssrv   = "1.3.6.1.5.5.7.8.7"; -- [SRV-ID]
+
+-- Compare a hostname (possibly international) with asserted names
+-- extracted from a certificate.
+-- This function follows the rules laid out in
+-- sections 4.4.1 and 4.4.2 of [TLS-CERTS]
+--
+-- A wildcard ("*") all by itself is allowed only as the left-most label
+local function compare_dnsname(host, asserted_names)
+	-- TODO: Sufficient normalization?  Review relevant specs.
+	local norm_host = idna_to_ascii(host)
+	if norm_host == nil then
+		log("info", "Host %s failed IDNA ToASCII operation", host)
+		return false
+	end
+
+	norm_host = norm_host:lower()
+
+	local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label
+
+	for i=1,#asserted_names do
+		local name = asserted_names[i]
+		if norm_host == name:lower() then
+			log("debug", "Cert dNSName %s matched hostname", name);
+			return true
+		end
+
+		-- Allow the left most label to be a "*"
+		if name:match("^%*%.") then
+			local rest_name = name:gsub("^[^.]+%.", "")
+			if host_chopped == rest_name:lower() then
+				log("debug", "Cert dNSName %s matched hostname", name);
+				return true
+			end
+		end
+	end
+
+	return false
+end
+
+-- Compare an XMPP domain name with the asserted id-on-xmppAddr
+-- identities extracted from a certificate.  Both are UTF8 strings.
+--
+-- Per [XMPP-CORE], matches against asserted identities don't include
+-- wildcards, so we just do a normalize on both and then a string comparison
+--
+-- TODO: Support for full JIDs?
+local function compare_xmppaddr(host, asserted_names)
+	local norm_host = nameprep(host)
+
+	for i=1,#asserted_names do
+		local name = asserted_names[i]
+
+		-- We only want to match against bare domains right now, not
+		-- those crazy full-er JIDs.
+		if name:match("[@/]") then
+			log("debug", "Ignoring xmppAddr %s because it's not a bare domain", name)
+		else
+			local norm_name = nameprep(name)
+			if norm_name == nil then
+				log("info", "Ignoring xmppAddr %s, failed nameprep!", name)
+			else
+				if norm_host == norm_name then
+					log("debug", "Cert xmppAddr %s matched hostname", name)
+					return true
+				end
+			end
+		end
+	end
+
+	return false
+end
+
+-- Compare a host + service against the asserted id-on-dnsSRV (SRV-ID)
+-- identities extracted from a certificate.
+--
+-- Per [SRV-ID], the asserted identities will be encoded in ASCII via ToASCII.
+-- Comparison is done case-insensitively, and a wildcard ("*") all by itself
+-- is allowed only as the left-most non-service label.
+local function compare_srvname(host, service, asserted_names)
+	local norm_host = idna_to_ascii(host)
+	if norm_host == nil then
+		log("info", "Host %s failed IDNA ToASCII operation", host);
+		return false
+	end
+
+	-- Service names start with a "_"
+	if service:match("^_") == nil then service = "_"..service end
+
+	norm_host = norm_host:lower();
+	local host_chopped = norm_host:gsub("^[^.]+%.", "") -- everything after the first label
+
+	for i=1,#asserted_names do
+		local asserted_service, name = asserted_names[i]:match("^(_[^.]+)%.(.*)");
+		if service == asserted_service then
+			if norm_host == name:lower() then
+				log("debug", "Cert SRVName %s matched hostname", name);
+				return true;
+			end
+
+			-- Allow the left most label to be a "*"
+			if name:match("^%*%.") then
+				local rest_name = name:gsub("^[^.]+%.", "")
+				if host_chopped == rest_name:lower() then
+					log("debug", "Cert SRVName %s matched hostname", name)
+					return true
+				end
+			end
+			if norm_host == name:lower() then
+				log("debug", "Cert SRVName %s matched hostname", name);
+				return true
+			end
+		end
+	end
+
+	return false
+end
+
+function verify_identity(host, service, cert)
+	local ext = cert:extensions()
+	if ext[oid_subjectaltname] then
+		local sans = ext[oid_subjectaltname];
+
+		-- Per [TLS-CERTS] 4.3, 4.4.4, "a client MUST NOT seek a match for a
+		-- reference identifier if the presented identifiers include a DNS-ID
+		-- SRV-ID, URI-ID, or any application-specific identifier types"
+		local had_supported_altnames = false
+
+		if sans[oid_xmppaddr] then
+			had_supported_altnames = true
+			if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
+		end
+
+		if sans[oid_dnssrv] then
+			had_supported_altnames = true
+			-- Only check srvNames if the caller specified a service
+			if service and compare_srvname(host, service, sans[oid_dnssrv]) then return true end
+		end
+
+		if sans["dNSName"] then
+			had_supported_altnames = true
+			if compare_dnsname(host, sans["dNSName"]) then return true end
+		end
+
+		-- We don't need URIs, but [TLS-CERTS] is clear.
+		if sans["uniformResourceIdentifier"] then
+			had_supported_altnames = true
+		end
+
+		if had_supported_altnames then return false end
+	end
+
+	-- Extract a common name from the certificate, and check it as if it were
+	-- a dNSName subjectAltName (wildcards may apply for, and receive,
+	-- cat treats)
+	--
+	-- Per [TLS-CERTS] 1.5, a CN-ID is the Common Name from a cert subject
+	-- which has one and only one Common Name
+	local subject = cert:subject()
+	local cn = nil
+	for i=1,#subject do
+		local dn = subject[i]
+		if dn["oid"] == oid_commonname then
+			if cn then
+				log("info", "Certificate has multiple common names")
+				return false
+			end
+
+			cn = dn["value"];
+		end
+	end
+
+	if cn then
+		-- Per [TLS-CERTS] 4.4.4, follow the comparison rules for dNSName SANs.
+		return compare_dnsname(host, { cn })
+	end
+
+	-- If all else fails, well, why should we be any different?
+	return false
+end
+
+return _M;
--- a/util/xmppstream.lua	Sun Jun 12 20:35:53 2011 +0100
+++ b/util/xmppstream.lua	Sun Jun 12 22:21:10 2011 +0100
@@ -9,10 +9,13 @@
 
 local lxp = require "lxp";
 local st = require "util.stanza";
+local stanza_mt = st.stanza_mt;
 
 local tostring = tostring;
 local t_insert = table.insert;
 local t_concat = table.concat;
+local t_remove = table.remove;
+local setmetatable = setmetatable;
 
 local default_log = require "util.logger".init("xmppstream");
 
@@ -63,12 +66,13 @@
 	
 	local stream_default_ns = stream_callbacks.default_ns;
 	
+	local stack = {};
 	local chardata, stanza = {};
 	local non_streamns_depth = 0;
 	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));
+			t_insert(stanza, t_concat(chardata));
 			chardata = {};
 		end
 		local curr_ns,name = tagname:match(ns_pattern);
@@ -112,9 +116,13 @@
 				cb_error(session, "invalid-top-level-element");
 			end
 			
-			stanza = st.stanza(name, attr);
+			stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt);
 		else -- we are inside a stanza, so add a tag
-			stanza:tag(name, attr);
+			t_insert(stack, stanza);
+			local oldstanza = stanza;
+			stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt);
+			t_insert(oldstanza, stanza);
+			t_insert(oldstanza.tags, stanza);
 		end
 	end
 	function xml_handlers:CharacterData(data)
@@ -129,12 +137,11 @@
 		if stanza then
 			if #chardata > 0 then
 				-- We have some character data in the buffer
-				stanza:text(t_concat(chardata));
+				t_insert(stanza, t_concat(chardata));
 				chardata = {};
 			end
 			-- Complete stanza
-			local last_add = stanza.last_add;
-			if not last_add or #last_add == 0 then
+			if #stack == 0 then
 				if tagname ~= stream_error_tag then
 					cb_handlestanza(session, stanza);
 				else
@@ -142,7 +149,7 @@
 				end
 				stanza = nil;
 			else
-				stanza:up();
+				stanza = t_remove(stack);
 			end
 		else
 			if tagname == stream_tag then
@@ -157,6 +164,7 @@
 				cb_error(session, "parse-error", "unexpected-element-close", name);
 			end
 			stanza, chardata = nil, {};
+			stack = {};
 		end
 	end
 
@@ -175,6 +183,7 @@
 	
 	local function reset()
 		stanza, chardata = nil, {};
+		stack = {};
 	end
 	
 	local function set_session(stream, new_session)