Changeset

4097:2d3866aed062

Merge 0.8->trunk
author Matthew Wild <mwild1@gmail.com>
date Fri, 07 Jan 2011 11:59:05 +0000
parents 4092:d78d5038d144 (diff) 4096:3b991ceb228e (current diff)
children 4098:7d687c348295
files
diffstat 9 files changed, 1232 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/core/certmanager.lua	Fri Jan 07 11:57:48 2011 +0000
+++ b/core/certmanager.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -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	Fri Jan 07 11:57:48 2011 +0000
+++ b/core/s2smanager.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -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,16 +393,47 @@
 		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);
@@ -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
--- a/plugins/mod_admin_telnet.lua	Fri Jan 07 11:57:48 2011 +0000
+++ b/plugins/mod_admin_telnet.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -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_dialback.lua	Fri Jan 07 11:57:48 2011 +0000
+++ b/plugins/mod_dialback.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -132,9 +132,19 @@
 	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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_pubsub.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -0,0 +1,370 @@
+local pubsub = require "util.pubsub";
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local uuid_generate = require "util.uuid".generate;
+
+require "core.modulemanager".load(module.host, "iq");
+
+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 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;
+	if jid_bare(jid) ~= jid_bare(stanza.attr.from) then
+		return origin.send(pubsub_error_reply(stanza, "invalid-jid"));
+	end
+	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;
+	if jid_bare(jid) ~= jid_bare(stanza.attr.from) then
+		return origin.send(pubsub_error_reply(stanza, "invalid-jid"));
+	end
+	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" }):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;
+}));
--- a/plugins/mod_saslauth.lua	Fri Jan 07 11:57:48 2011 +0000
+++ b/plugins/mod_saslauth.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -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;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/pubsub.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -0,0 +1,342 @@
+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";
+	
+	local node_capabilities = node_obj and node_obj.capabilities;
+	local service_capabilities = self.config.capabilities;
+	
+	-- Check if node allows/forbids it	
+	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 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, nil, jid);
+	if not jid_sub and not self:may(node, jid, "be_unsubscribed") then
+		local ok, err = self:add_subscription(node, nil, 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, nil, 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 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 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 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 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	Fri Jan 07 11:59:05 2011 +0000
@@ -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	Fri Jan 07 11:57:48 2011 +0000
+++ b/util/xmppstream.lua	Fri Jan 07 11:59:05 2011 +0000
@@ -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");
 
@@ -53,12 +56,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);
@@ -102,9 +106,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)
@@ -119,12 +127,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
@@ -132,7 +139,7 @@
 				end
 				stanza = nil;
 			else
-				stanza:up();
+				stanza = t_remove(stack);
 			end
 		else
 			if tagname == stream_tag then
@@ -147,11 +154,13 @@
 				cb_error(session, "parse-error", "unexpected-element-close", name);
 			end
 			stanza, chardata = nil, {};
+			stack = {};
 		end
 	end
 	
 	local function reset()
 		stanza, chardata = nil, {};
+		stack = {};
 	end
 	
 	local function set_session(stream, new_session)