Changeset

3901:4447c651e6e6

Merge with 0.8
author Matthew Wild <mwild1@gmail.com>
date Sun, 19 Dec 2010 03:04:42 +0000
parents 3899:eff0c5fe9119 (diff) 3900:e1f19f437ddd (current diff)
children 3902:a34333fcae72
files
diffstat 8 files changed, 793 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/core/certmanager.lua	Sun Dec 19 02:57:58 2010 +0000
+++ b/core/certmanager.lua	Sun Dec 19 03:04:42 2010 +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	Sun Dec 19 02:57:58 2010 +0000
+++ b/core/s2smanager.lua	Sun Dec 19 03:04:42 2010 +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;
@@ -373,16 +374,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);
@@ -407,6 +439,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());
@@ -426,7 +461,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	Sun Dec 19 02:57:58 2010 +0000
+++ b/plugins/mod_admin_telnet.lua	Sun Dec 19 03:04:42 2010 +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	Sun Dec 19 02:57:58 2010 +0000
+++ b/plugins/mod_dialback.lua	Sun Dec 19 03:04:42 2010 +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	Sun Dec 19 03:04:42 2010 +0000
@@ -0,0 +1,205 @@
+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 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" };
+};
+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 data = st.stanza("items", { node = node });
+	for _, entry in pairs(service:get(node, stanza.attr.from, id)) 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.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;
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:tag("create", { node = node });
+	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 = 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();
+
+module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
+	event.origin.send(st.reply(event.stanza):add_child(disco_info));
+	return true;
+end);
+
+module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
+	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);
+
+service = pubsub.new({
+	broadcaster = simple_broadcast
+});
+module.environment.service = service;
+
+function module.save()
+	return { service = service };
+end
+
+function module.restore(data)
+	service = data.service;
+	module.environment.service = service;
+end
--- a/plugins/mod_saslauth.lua	Sun Dec 19 02:57:58 2010 +0000
+++ b/plugins/mod_saslauth.lua	Sun Dec 19 03:04:42 2010 +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 t_concat, t_insert = table.concat, table.insert;
@@ -91,8 +94,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
@@ -168,6 +286,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	Sun Dec 19 03:04:42 2010 +0000
@@ -0,0 +1,84 @@
+module("pubsub", package.seeall);
+
+local service = {};
+local service_mt = { __index = service };
+
+function new(cb)
+	return setmetatable({ cb = cb or {}, nodes = {} }, service_mt);
+end
+
+function service:add_subscription(node, actor, jid)
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		return false, "item-not-found";
+	end
+	node_obj.subscribers[jid] = true;
+	return true;
+end
+
+function service:remove_subscription(node, actor, jid)
+	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;
+	return true;
+end
+
+function service:get_subscription(node, actor, jid)
+	local node_obj = self.nodes[node];
+	if node_obj then
+		return node_obj.subscribers[jid];
+	end
+end
+
+function service:create(node, actor)
+	if not self.nodes[node] then
+		self.nodes[node] = { name = node, subscribers = {}, config = {}, data = {} };
+		return true;
+	end
+	return false, "conflict";
+end
+
+function service:publish(node, actor, id, item)
+	local node_obj = self.nodes[node];
+	if not node_obj then
+		node_obj = { name = node, subscribers = {}, config = {}, data = {} };
+		self.nodes[node] = node_obj;
+	end
+	node_obj.data[id] = item;
+	self.cb.broadcaster(node, node_obj.subscribers, item);
+	return true;
+end
+
+function service:retract(node, actor, id, retract)
+	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.cb.broadcaster(node, node_obj.subscribers, retract);
+	end
+	return true
+end
+
+function service:get(node, actor, id)
+	local node_obj = self.nodes[node];
+	if node_obj then
+		if id then
+			return { node_obj.data[id] };
+		else
+			return node_obj.data;
+		end
+	end
+end
+
+function service:get_nodes(actor)
+	return true, self.nodes;
+end
+
+return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/x509.lua	Sun Dec 19 03:04:42 2010 +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;