Merge 0.9->trunk
authorMatthew Wild <mwild1@gmail.com>
Wed, 22 May 2013 14:33:18 +0100
changeset 5622 8bd1548a48b1
parent 5620 8349ae2a44ce (diff)
parent 5621 63cfd59999b6 (current diff)
child 5623 5752391fa27d
Merge 0.9->trunk
--- a/plugins/mod_admin_telnet.lua	Wed May 22 14:32:02 2013 +0100
+++ b/plugins/mod_admin_telnet.lua	Wed May 22 14:33:18 2013 +0100
@@ -236,6 +236,7 @@
 	elseif section == "server" then
 		print [[server:version() - Show the server's version number]]
 		print [[server:uptime() - Show how long the server has been running]]
+		print [[server:memory() - Show details about the server's memory usage]]
 		print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]]
 	elseif section == "port" then
 		print [[port:list() - Lists all network ports prosody currently listens on]]
@@ -300,6 +301,26 @@
 	return true, "Shutdown initiated";
 end
 
+local function human(kb)
+	local unit = "K";
+	if kb > 1024 then
+		kb, unit = kb/1024, "M";
+	end
+	return ("%0.2f%sB"):format(kb, unit);
+end
+
+function def_env.server:memory()
+	if not pposix.meminfo then
+		return true, "Lua is using "..collectgarbage("count");
+	end
+	local mem, lua_mem = pposix.meminfo(), collectgarbage("count");
+	local print = self.session.print;
+	print("Process: "..human((mem.allocated+mem.allocated_mmap)/1024));
+	print("   Used: "..human(mem.used/1024).." ("..human(lua_mem).." by Lua)");
+	print("   Free: "..human(mem.unused/1024).." ("..human(mem.returnable/1024).." returnable)");
+	return true, "OK";
+end
+
 def_env.module = {};
 
 local function get_hosts_set(hosts, module)
@@ -463,6 +484,25 @@
 function def_env.hosts:add(name)
 end
 
+local function session_flags(session, line)
+	line = line or {};
+	if session.cert_identity_status == "valid" then
+		line[#line+1] = "(secure)";
+	elseif session.secure then
+		line[#line+1] = "(encrypted)";
+	end
+	if session.compressed then
+		line[#line+1] = "(compressed)";
+	end
+	if session.smacks then
+		line[#line+1] = "(sm)";
+	end
+	if (session.ip or session.conn and session.conn:ip()):match(":") then
+		line[#line+1] = "(IPv6)";
+	end
+	return table.concat(line, " ");
+end
+
 def_env.c2s = {};
 
 local function show_c2s(callback)
@@ -498,14 +538,9 @@
 			count = count + 1;
 			local status, priority = "unavailable", tostring(session.priority or "-");
 			if session.presence then
-				status = session.presence:child_with_name("show");
-				if status then
-					status = status:get_text() or "[invalid!]";
-				else
-					status = "available";
-				end
+				status = session.presence:get_child_text("show") or "available";
 			end
-			print("   "..jid.." - "..status.."("..priority..")");
+			print(session_flags(session, { "   "..jid.." - "..status.."("..priority..")" }));
 		end		
 	end);
 	return true, "Total: "..count.." clients";
@@ -544,23 +579,6 @@
 	return true, "Total: "..count.." sessions closed";
 end
 
-local function session_flags(session, line)
-	if session.cert_identity_status == "valid" then
-		line[#line+1] = "(secure)";
-	elseif session.secure then
-		line[#line+1] = "(encrypted)";
-	end
-	if session.compressed then
-		line[#line+1] = "(compressed)";
-	end
-	if session.smacks then
-		line[#line+1] = "(sm)";
-	end
-	if session.conn and session.conn:ip():match(":") then
-		line[#line+1] = "(IPv6)";
-	end
-	return table.concat(line, " ");
-end
 
 def_env.s2s = {};
 function def_env.s2s:show(match_jid)
--- a/plugins/muc/mod_muc.lua	Wed May 22 14:32:02 2013 +0100
+++ b/plugins/muc/mod_muc.lua	Wed May 22 14:33:18 2013 +0100
@@ -115,7 +115,7 @@
 local function get_disco_items(stanza)
 	local reply = st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
 	for jid, room in pairs(rooms) do
-		if not room:is_hidden() then
+		if not room:get_hidden() then
 			reply:tag("item", {jid=jid, name=room:get_name()}):up();
 		end
 	end
--- a/plugins/muc/muc.lib.lua	Wed May 22 14:32:02 2013 +0100
+++ b/plugins/muc/muc.lib.lua	Wed May 22 14:33:18 2013 +0100
@@ -27,28 +27,16 @@
 local default_history_length, max_history_length = 20, math.huge;
 
 ------------
-local function filter_xmlns_from_array(array, filters)
-	local count = 0;
-	for i=#array,1,-1 do
-		local attr = array[i].attr;
-		if filters[attr and attr.xmlns] then
-			t_remove(array, i);
-			count = count + 1;
-		end
+local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+local function presence_filter(tag)
+	if presence_filters[tag.attr.xmlns] then
+		return nil;
 	end
-	return count;
+	return tag;
 end
-local function filter_xmlns_from_stanza(stanza, filters)
-	if filters then
-		if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then
-			return stanza, filter_xmlns_from_array(stanza, filters);
-		end
-	end
-	return stanza, 0;
-end
-local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
+
 local function get_filtered_presence(stanza)
-	return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters);
+	return st.clone(stanza):maptags(presence_filter);
 end
 local kickable_error_conditions = {
 	["gone"] = true;
@@ -72,17 +60,6 @@
 	local cond = get_error_condition(stanza);
 	return kickable_error_conditions[cond] and cond;
 end
-local function getUsingPath(stanza, path, getText)
-	local tag = stanza;
-	for _, name in ipairs(path) do
-		if type(tag) ~= 'table' then return; end
-		tag = tag:child_with_name(name);
-	end
-	if tag and getText then tag = table.concat(tag); end
-	return tag;
-end
-local function getTag(stanza, path) return getUsingPath(stanza, path); end
-local function getText(stanza, path) return getUsingPath(stanza, path, true); end
 -----------
 
 local room_mt = {};
@@ -98,8 +75,8 @@
 	elseif affiliation == "member" then
 		return "participant";
 	elseif not affiliation then
-		if not self:is_members_only() then
-			return self:is_moderated() and "visitor" or "participant";
+		if not self:get_members_only() then
+			return self:get_moderated() and "visitor" or "participant";
 		end
 	end
 end
@@ -218,10 +195,10 @@
 		:tag("identity", {category="conference", type="text", name=self:get_name()}):up()
 		:tag("feature", {var="http://jabber.org/protocol/muc"}):up()
 		:tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up()
-		:tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
-		:tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up()
-		:tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up()
-		:tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up()
+		:tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
+		:tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up()
+		:tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up()
+		:tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up()
 		: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" },
@@ -238,7 +215,6 @@
 	return reply;
 end
 function room_mt:set_subject(current_nick, subject)
-	-- TODO check nick's authority
 	if subject == "" then subject = nil; end
 	self._data['subject'] = subject;
 	self._data['subject_from'] = current_nick;
@@ -296,7 +272,7 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_moderated()
+function room_mt:get_moderated()
 	return self._data.moderated;
 end
 function room_mt:set_members_only(members_only)
@@ -306,7 +282,7 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_members_only()
+function room_mt:get_members_only()
 	return self._data.members_only;
 end
 function room_mt:set_persistent(persistent)
@@ -316,7 +292,7 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_persistent()
+function room_mt:get_persistent()
 	return self._data.persistent;
 end
 function room_mt:set_hidden(hidden)
@@ -326,9 +302,15 @@
 		if self.save then self:save(true); end
 	end
 end
-function room_mt:is_hidden()
+function room_mt:get_hidden()
 	return self._data.hidden;
 end
+function room_mt:get_public()
+	return not self:get_hidden();
+end
+function room_mt:set_public(public)
+	return self:set_hidden(not public);
+end
 function room_mt:set_changesubject(changesubject)
 	changesubject = changesubject and true or nil;
 	if self._data.changesubject ~= changesubject then
@@ -351,6 +333,19 @@
 end
 
 
+local valid_whois = { moderators = true, anyone = true };
+
+function room_mt:set_whois(whois)
+	if valid_whois[whois] and self._data.whois ~= whois then
+		self._data.whois = whois;
+		if self.save then self:save(true); end
+	end
+end
+
+function room_mt:get_whois()
+	return self._data.whois;
+end
+
 local function construct_stanza_id(room, stanza)
 	local from_jid, to_nick = stanza.attr.from, stanza.attr.to;
 	local from_nick = room._jid_nick[from_jid];
@@ -575,11 +570,11 @@
 
 function room_mt:send_form(origin, stanza)
 	origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
-		:add_child(self:get_form_layout():form())
+		:add_child(self:get_form_layout(stanza.attr.from):form())
 	);
 end
 
-function room_mt:get_form_layout()
+function room_mt:get_form_layout(actor)
 	local form = dataform.new({
 		title = "Configuration for "..self.jid,
 		instructions = "Complete and submit this form to configure the room.",
@@ -604,13 +599,13 @@
 			name = 'muc#roomconfig_persistentroom',
 			type = 'boolean',
 			label = 'Make Room Persistent?',
-			value = self:is_persistent()
+			value = self:get_persistent()
 		},
 		{
 			name = 'muc#roomconfig_publicroom',
 			type = 'boolean',
 			label = 'Make Room Publicly Searchable?',
-			value = not self:is_hidden()
+			value = not self:get_hidden()
 		},
 		{
 			name = 'muc#roomconfig_changesubject',
@@ -637,13 +632,13 @@
 			name = 'muc#roomconfig_moderatedroom',
 			type = 'boolean',
 			label = 'Make Room Moderated?',
-			value = self:is_moderated()
+			value = self:get_moderated()
 		},
 		{
 			name = 'muc#roomconfig_membersonly',
 			type = 'boolean',
 			label = 'Make Room Members-Only?',
-			value = self:is_members_only()
+			value = self:get_members_only()
 		},
 		{
 			name = 'muc#roomconfig_historylength',
@@ -652,14 +647,9 @@
 			value = tostring(self:get_historylength())
 		}
 	});
-	return module:fire_event("muc-config-form", { room = self, form = form }) or form;
+	return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form;
 end
 
-local valid_whois = {
-	moderators = true,
-	anyone = true,
-}
-
 function room_mt:process_form(origin, stanza)
 	local query = stanza.tags[1];
 	local form;
@@ -668,84 +658,46 @@
 	if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end
 	if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end
 
-	local fields = self:get_form_layout():data(form);
+	local fields = self:get_form_layout(stanza.attr.from):data(form);
 	if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end
 
-	local dirty = false
 
-	local event = { room = self, fields = fields, changed = dirty };
-	module:fire_event("muc-config-submitted", event);
-	dirty = event.changed or dirty;
+	local changed = {};
 
-	local name = fields['muc#roomconfig_roomname'];
-	if name ~= self:get_name() then
-		self:set_name(name);
-	end
-
-	local description = fields['muc#roomconfig_roomdesc'];
-	if description ~= self:get_description() then
-		self:set_description(description);
+	local function handle_option(name, field, allowed)
+		local new = fields[field];
+		if new == nil then return; end
+		if allowed and not allowed[new] then return; end
+		if new == self["get_"..name](self) then return; end
+		changed[name] = true;
+		self["set_"..name](self, new);
 	end
 
-	local persistent = fields['muc#roomconfig_persistentroom'];
-	dirty = dirty or (self:is_persistent() ~= persistent)
-	module:log("debug", "persistent=%s", tostring(persistent));
-
-	local moderated = fields['muc#roomconfig_moderatedroom'];
-	dirty = dirty or (self:is_moderated() ~= moderated)
-	module:log("debug", "moderated=%s", tostring(moderated));
-
-	local membersonly = fields['muc#roomconfig_membersonly'];
-	dirty = dirty or (self:is_members_only() ~= membersonly)
-	module:log("debug", "membersonly=%s", tostring(membersonly));
-
-	local public = fields['muc#roomconfig_publicroom'];
-	dirty = dirty or (self:is_hidden() ~= (not public and true or nil))
-
-	local changesubject = fields['muc#roomconfig_changesubject'];
-	dirty = dirty or (self:get_changesubject() ~= (not changesubject and true or nil))
-	module:log('debug', 'changesubject=%s', changesubject and "true" or "false")
+	local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option };
+	module:fire_event("muc-config-submitted", event);
 
-	local historylength = tonumber(fields['muc#roomconfig_historylength']);
-	dirty = dirty or (historylength and (self:get_historylength() ~= historylength));
-	module:log('debug', 'historylength=%s', historylength)
-
-
-	local whois = fields['muc#roomconfig_whois'];
-	if not valid_whois[whois] then
-	    origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'"));
-	    return;
-	end
-	local whois_changed = self._data.whois ~= whois
-	self._data.whois = whois
-	module:log('debug', 'whois=%s', whois)
-
-	local password = fields['muc#roomconfig_roomsecret'];
-	if self:get_password() ~= password then
-		self:set_password(password);
-	end
-	self:set_moderated(moderated);
-	self:set_members_only(membersonly);
-	self:set_persistent(persistent);
-	self:set_hidden(not public);
-	self:set_changesubject(changesubject);
-	self:set_historylength(historylength);
+	handle_option("name", "muc#roomconfig_roomname");
+	handle_option("description", "muc#roomconfig_roomdesc");
+	handle_option("persistent", "muc#roomconfig_persistentroom");
+	handle_option("moderated", "muc#roomconfig_moderatedroom");
+	handle_option("members_only", "muc#roomconfig_membersonly");
+	handle_option("public", "muc#roomconfig_publicroom");
+	handle_option("changesubject", "muc#roomconfig_changesubject");
+	handle_option("historylength", "muc#roomconfig_historylength");
+	handle_option("whois", "muc#roomconfig_whois", valid_whois);
+	handle_option("password", "muc#roomconfig_roomsecret");
 
 	if self.save then self:save(true); end
 	origin.send(st.reply(stanza));
 
-	if dirty or whois_changed then
+	if next(changed) then
 		local msg = st.message({type='groupchat', from=self.jid})
 			:tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up()
-
-		if dirty then
-			msg.tags[1]:tag('status', {code = '104'}):up();
-		end
-		if whois_changed then
-			local code = (whois == 'moderators') and "173" or "172";
+				:tag('status', {code = '104'}):up();
+		if changed.whois then
+			local code = (self:get_whois() == 'moderators') and "173" or "172";
 			msg.tags[1]:tag('status', {code = code}):up();
 		end
-
 		self:broadcast_message(msg, false)
 	end
 end
@@ -881,7 +833,7 @@
 			origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
 		end
 	elseif stanza.name == "message" and type == "groupchat" then
-		local from, to = stanza.attr.from, stanza.attr.to;
+		local from = stanza.attr.from;
 		local current_nick = self._jid_nick[from];
 		local occupant = self._occupants[current_nick];
 		if not occupant then -- not in room
@@ -891,11 +843,11 @@
 		else
 			local from = stanza.attr.from;
 			stanza.attr.from = current_nick;
-			local subject = getText(stanza, {"subject"});
+			local subject = stanza:get_child_text("subject");
 			if subject then
 				if occupant.role == "moderator" or
 					( self._data.changesubject and occupant.role == "participant" ) then -- and participant
-					self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
+					self:set_subject(current_nick, subject);
 				else
 					stanza.attr.from = from;
 					origin.send(st.error_reply(stanza, "auth", "forbidden"));
@@ -943,7 +895,7 @@
 					:tag('body') -- Add a plain message for clients which don't support invites
 						:text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
 					:up();
-				if self:is_members_only() and not self:get_affiliation(_invitee) then
+				if self:get_members_only() and not self:get_affiliation(_invitee) then
 					log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to);
 					self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from])
 				end
@@ -1055,7 +1007,7 @@
 end
 function room_mt:can_set_role(actor_jid, occupant_jid, role)
 	local occupant = self._occupants[occupant_jid];
-	if not occupant or not actor then return nil, "modify", "not-acceptable"; end
+	if not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end
 
 	if actor_jid == true then return true; end
 
--- a/prosody.cfg.lua.dist	Wed May 22 14:32:02 2013 +0100
+++ b/prosody.cfg.lua.dist	Wed May 22 14:33:18 2013 +0100
@@ -4,7 +4,7 @@
 -- website at http://prosody.im/doc/configure
 --
 -- Tip: You can check that the syntax of this file is correct
--- when you have finished by running: luac -p prosody.cfg.lua
+-- when you have finished by running: prosodyctl check config
 -- If there are any errors, it will let you know what and where
 -- they are, otherwise it will keep quiet.
 --
--- a/prosodyctl	Wed May 22 14:32:02 2013 +0100
+++ b/prosodyctl	Wed May 22 14:33:18 2013 +0100
@@ -274,11 +274,12 @@
 local command = arg[1];
 
 function commands.adduser(arg)
+	local jid_split = require "util.jid".split;
 	if not arg[1] or arg[1] == "--help" then
 		show_usage([[adduser JID]], [[Create the specified user account in Prosody]]);
 		return 1;
 	end
-	local user, host = arg[1]:match("([^@]+)@(.+)");
+	local user, host = jid_split(arg[1]);
 	if not user and host then
 		show_message [[Failed to understand JID, please supply the JID you want to create]]
 		show_usage [[adduser user@host]]
@@ -313,11 +314,12 @@
 end
 
 function commands.passwd(arg)
+	local jid_split = require "util.jid".split;
 	if not arg[1] or arg[1] == "--help" then
 		show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]);
 		return 1;
 	end
-	local user, host = arg[1]:match("([^@]+)@(.+)");
+	local user, host = jid_split(arg[1]);
 	if not user and host then
 		show_message [[Failed to understand JID, please supply the JID you want to set the password for]]
 		show_usage [[passwd user@host]]
@@ -352,11 +354,12 @@
 end
 
 function commands.deluser(arg)
+	local jid_split = require "util.jid".split;
 	if not arg[1] or arg[1] == "--help" then
 		show_usage([[deluser JID]], [[Permanently remove the specified user account from Prosody]]);
 		return 1;
 	end
-	local user, host = arg[1]:match("([^@]+)@(.+)");
+	local user, host = jid_split(arg[1]);
 	if not user and host then
 		show_message [[Failed to understand JID, please supply the JID you want to set the password for]]
 		show_usage [[passwd user@host]]
@@ -776,6 +779,253 @@
 	show_usage("cert config|request|generate|key", "Helpers for generating X.509 certificates and keys.")
 end
 
+function commands.check(arg)
+	local what = table.remove(arg, 1);
+	local array, set = require "util.array", require "util.set";
+	local it = require "util.iterators";
+	local ok = true;
+	if not what or what == "config" then
+		print("Checking config...");
+		local known_global_options = set.new({
+			"pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize",
+			"umask", "prosodyctl_timeout", "use_ipv6", "use_libevent", "network_settings"
+		});
+		local config = config.getconfig();
+		-- Check that we have any global options (caused by putting a host at the top)
+		if it.count(it.filter("log", pairs(config["*"]))) == 0 then
+			ok = false;
+			print("");
+			print("    No global options defined. Perhaps you have put a host definition at the top")
+			print("    of the config file? They should be at the bottom, see http://prosody.im/doc/configure#overview");
+		end
+		-- Check for global options under hosts
+		local global_options = set.new(it.to_array(it.keys(config["*"])));
+		for host, options in it.filter("*", pairs(config)) do
+			local host_options = set.new(it.to_array(it.keys(options)));
+			local misplaced_options = set.intersection(host_options, known_global_options);
+			for name in pairs(options) do
+				if name:match("^interfaces?")
+				or name:match("_ports?$") or name:match("_interfaces?$")
+				or name:match("_ssl$") then
+					misplaced_options:add(name);
+				end
+			end
+			if not misplaced_options:empty() then
+				ok = false;
+				print("");
+				local n = it.count(misplaced_options);
+				print("    You have "..n.." option"..(n>1 and "s " or " ").."set under "..host.." that should be");
+				print("    in the global section of the config file, above any VirtualHost or Component definitions,")
+				print("    see http://prosody.im/doc/configure#overview for more information.")
+				print("");
+				print("    You need to move the following option"..(n>1 and "s" or "")..": "..table.concat(it.to_array(misplaced_options), ", "));
+			end
+			local subdomain = host:match("^[^.]+");
+			if not(host_options:contains("component_module")) and (subdomain == "jabber" or subdomain == "xmpp"
+			   or subdomain == "chat" or subdomain == "im") then
+			   	print("");
+			   	print("    Suggestion: If "..host.. " is a new host with no real users yet, consider renaming it now to");
+			   	print("     "..host:gsub("^[^.]+%.", "")..". You can use SRV records to redirect XMPP clients and servers to "..host..".");
+			   	print("     For more information see: http://prosody.im/doc/dns");
+			end
+		end
+		
+		print("Done.\n");
+	end
+	if not what or what == "dns" then
+		local dns = require "net.dns";
+		local ip = require "util.ip";
+		local c2s_ports = set.new(config.get("*", "c2s_ports") or {5222});
+		local s2s_ports = set.new(config.get("*", "s2s_ports") or {5269});
+		
+		local c2s_srv_required, s2s_srv_required;
+		if not c2s_ports:contains(5222) then
+			c2s_srv_required = true;
+		end
+		if not s2s_ports:contains(5269) then
+			s2s_srv_required = true;
+		end
+		
+		local problem_hosts = set.new();
+		
+		local external_addresses, internal_addresses = set.new(), set.new();
+		
+		local fqdn = socket.dns.tohostname(socket.dns.gethostname());
+		if fqdn then
+			local res = dns.lookup(fqdn, "A");
+			if res then
+				for _, record in ipairs(res) do
+					external_addresses:add(record.a);
+				end
+			end
+			local res = dns.lookup(fqdn, "AAAA");
+			if res then
+				for _, record in ipairs(res) do
+					external_addresses:add(record.aaaa);
+				end
+			end
+		end
+		
+		local local_addresses = socket.local_addresses and socket.local_addresses() or {};
+		
+		for addr in it.values(local_addresses) do
+			if not ip.new_ip(addr).private then
+				external_addresses:add(addr);
+			else
+				internal_addresses:add(addr);
+			end
+		end
+		
+		if external_addresses:empty() then
+			print("");
+			print("   Failed to determine the external addresses of this server. Checks may be inaccurate.");
+			c2s_srv_required, s2s_srv_required = true, true;
+		end
+		
+		local v6_supported = not not socket.tcp6;
+		
+		for host, host_options in it.filter("*", pairs(config.getconfig())) do
+			local all_targets_ok, some_targets_ok = true, false;
+			
+			local is_component = not not host_options.component_module;
+			print("Checking DNS for "..(is_component and "component" or "host").." "..host.."...");
+			local target_hosts = set.new();
+			if not is_component then
+				local res = dns.lookup("_xmpp-client._tcp."..host..".", "SRV");
+				if res then
+					for _, record in ipairs(res) do
+						target_hosts:add(record.srv.target);
+						if not c2s_ports:contains(record.srv.port) then
+							print("    SRV target "..record.srv.target.." contains unknown client port: "..record.srv.port);
+						end
+					end
+				else
+					if c2s_srv_required then
+						print("    No _xmpp-client SRV record found for "..host..", but it looks like you need one.");
+						all_targst_ok = false;
+					else
+						target_hosts:add(host);
+					end
+				end
+			end
+			local res = dns.lookup("_xmpp-server._tcp."..host..".", "SRV");
+			if res then
+				for _, record in ipairs(res) do
+					target_hosts:add(record.srv.target);
+					if not s2s_ports:contains(record.srv.port) then
+						print("    SRV target "..record.srv.target.." contains unknown server port: "..record.srv.port);
+					end
+				end
+			else
+				if s2s_srv_required then
+					print("    No _xmpp-server SRV record found for "..host..", but it looks like you need one.");
+					all_targets_ok = false;
+				else
+					target_hosts:add(host);
+				end
+			end
+			if target_hosts:empty() then
+				target_hosts:add(host);
+			end
+			
+			if target_hosts:contains("localhost") then
+				print("    Target 'localhost' cannot be accessed from other servers");
+				target_hosts:remove("localhost");
+			end
+			
+			local modules = set.new(it.to_array(it.values(host_options.modules_enabled)))
+			                + set.new(it.to_array(it.values(config.get("*", "modules_enabled"))))
+			                + set.new({ config.get(host, "component_module") });
+
+			if modules:contains("proxy65") then
+				local proxy65_target = config.get(host, "proxy65_address") or host;
+				local A, AAAA = dns.lookup(proxy65_target, "A"), dns.lookup(proxy65_target, "AAAA");
+				local prob = {};
+				if not A then
+					table.insert(prob, "A");
+				end
+				if v6_supported and not AAAA then
+					table.insert(prob, "AAAA");
+				end
+				if #prob > 0 then
+					print("    File transfer proxy "..proxy65_target.." has no "..table.concat(prob, "/").." record. Create one or set 'proxy65_address' to the correct host/IP.");
+				end
+			end
+			
+			for host in target_hosts do
+				local host_ok_v4, host_ok_v6;
+				local res = dns.lookup(host, "A");
+				if res then
+					for _, record in ipairs(res) do
+						if external_addresses:contains(record.a) then
+							some_targets_ok = true;
+							host_ok_v4 = true;
+						elseif internal_addresses:contains(record.a) then
+							host_ok_v4 = true;
+							some_targets_ok = true;
+							print("    "..host.." A record points to internal address, external connections might fail");
+						else
+							print("    "..host.." A record points to unknown address "..record.a);
+							all_targets_ok = false;
+						end
+					end
+				end
+				local res = dns.lookup(host, "AAAA");
+				if res then
+					for _, record in ipairs(res) do
+						if external_addresses:contains(record.aaaa) then
+							some_targets_ok = true;
+							host_ok_v6 = true;
+						elseif internal_addresses:contains(record.aaaa) then
+							host_ok_v6 = true;
+							some_targets_ok = true;
+							print("    "..host.." AAAA record points to internal address, external connections might fail");
+						else
+							print("    "..host.." AAAA record points to unknown address "..record.aaaa);
+							all_targets_ok = false;
+						end
+					end
+				end
+				
+				local bad_protos = {}
+				if not host_ok_v4 then
+					table.insert(bad_protos, "IPv4");
+				end
+				if not host_ok_v6 then
+					table.insert(bad_protos, "IPv6");
+				end
+				if #bad_protos > 0 then
+					print("    Host "..host.." does not seem to resolve to this server ("..table.concat(bad_protos, "/")..")");
+				end
+				if host_ok_v6 and not v6_supported then
+					print("    Host "..host.." has AAAA records, but your version of LuaSocket does not support IPv6.");
+					print("      Please see http://prosody.im/doc/ipv6 for more information.");
+				end
+			end
+			if not all_targets_ok then
+				print("    "..(some_targets_ok and "Only some" or "No").." targets for "..host.." appear to resolve to this server.");
+				if is_component then
+					print("    DNS records are necessary if you want users on other servers to access this component.");
+				end
+				problem_hosts:add(host);
+			end
+			print("");
+		end
+		if not problem_hosts:empty() then
+			print("");
+			print("For more information about DNS configuration please see http://prosody.im/doc/dns");
+			print("");
+			ok = false;
+		end
+	end
+	if not ok then
+		print("Problems found, see above.");
+	else
+		print("All checks passed, congratulations!");
+	end
+	return ok and 0 or 2;
+end
+
 ---------------------
 
 if command and command:match("^mod_") then -- Is a command in a module
--- a/tests/test.lua	Wed May 22 14:32:02 2013 +0100
+++ b/tests/test.lua	Wed May 22 14:33:18 2013 +0100
@@ -12,12 +12,12 @@
 	package.loaded["net.connlisteners"] = { get = function () return {} end };
 	dotest "util.jid"
 	dotest "util.multitable"
-	dotest "util.rfc3484"
-	dotest "net.http"
-	dotest "core.modulemanager"
+	dotest "util.rfc6724"
+	dotest "util.http"
 	dotest "core.stanza_router"
 	dotest "core.s2smanager"
 	dotest "core.configmanager"
+	dotest "util.ip"
 	dotest "util.stanza"
 	dotest "util.sasl.scram"
 	
@@ -136,15 +136,21 @@
 	end
 	
 	local oldmodule, old_M = _fakeG.module, _fakeG._M;
-	_fakeG.module = function () _M = _G end
+	_fakeG.module = function () _M = unit end
 	setfenv(chunk, unit);
-	local success, err = pcall(chunk);
+	local success, ret = pcall(chunk);
 	_fakeG.module, _fakeG._M = oldmodule, old_M;
 	if not success then
 		print("WARNING: ", "Failed to initialise module: "..unitname, err);
 		return;
 	end
 	
+	if type(ret) == "table" then
+		for k,v in pairs(ret) do
+			unit[k] = v;
+		end
+	end
+
 	for name, f in pairs(unit) do
 		local test = rawget(tests, name);
 		if type(f) ~= "function" then
--- a/tests/test_core_configmanager.lua	Wed May 22 14:32:02 2013 +0100
+++ b/tests/test_core_configmanager.lua	Wed May 22 14:33:18 2013 +0100
@@ -9,27 +9,23 @@
 
 
 function get(get, config)
-	config.set("example.com", "test", "testkey", 123);
-	assert_equal(get("example.com", "test", "testkey"), 123, "Retrieving a set key");
+	config.set("example.com", "testkey", 123);
+	assert_equal(get("example.com", "testkey"), 123, "Retrieving a set key");
 
-	config.set("*", "test", "testkey1", 321);
-	assert_equal(get("*", "test", "testkey1"), 321, "Retrieving a set global key");
-	assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists");
+	config.set("*", "testkey1", 321);
+	assert_equal(get("*", "testkey1"), 321, "Retrieving a set global key");
+	assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key of undefined host, of which only a globally set one exists");
 	
-	config.set("example.com", "test", ""); -- Creates example.com host in config
-	assert_equal(get("example.com", "test", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists");
+	config.set("example.com", ""); -- Creates example.com host in config
+	assert_equal(get("example.com", "testkey1"), 321, "Retrieving a set key, of which only a globally set one exists");
 	
 	assert_equal(get(), nil, "No parameters to get()");
 	assert_equal(get("undefined host"), nil, "Getting for undefined host");
-	assert_equal(get("undefined host", "undefined section"), nil, "Getting for undefined host & section");
-	assert_equal(get("undefined host", "undefined section", "undefined key"), nil, "Getting for undefined host & section & key");
-
-	assert_equal(get("example.com", "undefined section", "testkey"), nil, "Defined host, undefined section");
+	assert_equal(get("undefined host", "undefined key"), nil, "Getting for undefined host & key");
 end
 
 function set(set, u)
-	assert_equal(set("*"), false, "Set with no section/key");
-	assert_equal(set("*", "set_test"), false, "Set with no key");
+	assert_equal(set("*"), false, "Set with no key");
 
 	assert_equal(set("*", "set_test", "testkey"), true, "Setting a nil global value");
 	assert_equal(set("*", "set_test", "testkey", 123), true, "Setting a global value");
--- a/tests/test_core_modulemanager.lua	Wed May 22 14:32:02 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
--- 
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-local config = require "core.configmanager";
-local helpers = require "util.helpers";
-local set = require "util.set";
-
-function load_modules_for_host(load_modules_for_host, mm)
-	local test_num = 0;
-	local function test_load(global_modules_enabled, global_modules_disabled, host_modules_enabled, host_modules_disabled, expected_modules)
-		test_num = test_num + 1;
-		-- Prepare
-		hosts = { ["example.com"] = {} };
-		config.set("*", "core", "modules_enabled", global_modules_enabled);
-		config.set("*", "core", "modules_disabled", global_modules_disabled);
-		config.set("example.com", "core", "modules_enabled", host_modules_enabled);
-		config.set("example.com", "core", "modules_disabled", host_modules_disabled);
-		
-		expected_modules = set.new(expected_modules);
-		expected_modules:add_list(helpers.get_upvalue(load_modules_for_host, "autoload_modules"));
-		
-		local loaded_modules = set.new();
-		function mm.load(host, module)
-			assert_equal(host, "example.com", test_num..": Host isn't example.com but "..tostring(host));
-			assert_equal(expected_modules:contains(module), true, test_num..": Loading unexpected module '"..tostring(module).."'");
-			loaded_modules:add(module);
-		end
-		load_modules_for_host("example.com");
-		assert_equal((expected_modules - loaded_modules):empty(), true, test_num..": Not all modules loaded: "..tostring(expected_modules - loaded_modules));
-	end
-	
-	test_load({ "one", "two", "three" }, nil, nil, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, {}, nil, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, { "two" }, nil, nil, { "one", "three" });
-	test_load({ "one", "two", "three" }, { "three" }, nil, nil, { "one", "two" });
-	test_load({ "one", "two", "three" }, nil, nil, { "three" }, { "one", "two" });
-	test_load({ "one", "two", "three" }, nil, { "three" }, { "three" }, { "one", "two", "three" });
-
-	test_load({ "one", "two" }, nil, { "three" }, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, nil, { "three" }, nil, { "one", "two", "three" });
-	test_load({ "one", "two", "three" }, { "three" }, { "three" }, nil, { "one", "two", "three" });
-	test_load({ "one", "two" }, { "three" }, { "three" }, nil, { "one", "two", "three" });
-end
--- a/tests/test_core_s2smanager.lua	Wed May 22 14:32:02 2013 +0100
+++ b/tests/test_core_s2smanager.lua	Wed May 22 14:33:18 2013 +0100
@@ -6,6 +6,9 @@
 -- COPYING file in the source package for more information.
 --
 
+env = {
+	prosody = { events = require "util.events".new() };
+};
 
 function compare_srv_priorities(csp)
 	local r1 = { priority = 10, weight = 0 }
--- a/tests/test_net_http.lua	Wed May 22 14:32:02 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
--- Prosody IM
--- Copyright (C) 2008-2010 Matthew Wild
--- Copyright (C) 2008-2010 Waqas Hussain
--- 
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-function urlencode(urlencode)
-	assert_equal(urlencode("helloworld123"), "helloworld123", "Normal characters not escaped");
-	assert_equal(urlencode("hello world"), "hello%20world", "Spaces escaped");
-	assert_equal(urlencode("This & that = something"), "This%20%26%20that%20%3d%20something", "Important URL chars escaped");
-end
-
-function urldecode(urldecode)
-	assert_equal("helloworld123", urldecode("helloworld123"), "Normal characters not escaped");
-	assert_equal("hello world", urldecode("hello%20world"), "Spaces escaped");
-	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
-	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3D%20something"), "Important URL chars escaped");
-end
-
-function formencode(formencode)
-	assert_equal(formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded");
-	assert_equal(formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
-end
-
-function formdecode(formdecode)
-	local t = formdecode("one=1&two=2");
-	assert_table(t[1]);
-	assert_equal(t[1].name, "one"); assert_equal(t[1].value, "1");
-	assert_table(t[2]);
-	assert_equal(t[2].name, "two"); assert_equal(t[2].value, "2");
-
-	local t = formdecode("one+two=1&two+one%26=2");
-	assert_equal(t[1].name, "one two"); assert_equal(t[1].value, "1");
-	assert_equal(t[2].name, "two one&"); assert_equal(t[2].value, "2");
-end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_http.lua	Wed May 22 14:33:18 2013 +0100
@@ -0,0 +1,37 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- 
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+function urlencode(urlencode)
+	assert_equal(urlencode("helloworld123"), "helloworld123", "Normal characters not escaped");
+	assert_equal(urlencode("hello world"), "hello%20world", "Spaces escaped");
+	assert_equal(urlencode("This & that = something"), "This%20%26%20that%20%3d%20something", "Important URL chars escaped");
+end
+
+function urldecode(urldecode)
+	assert_equal("helloworld123", urldecode("helloworld123"), "Normal characters not escaped");
+	assert_equal("hello world", urldecode("hello%20world"), "Spaces escaped");
+	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
+	assert_equal("This & that = something", urldecode("This%20%26%20that%20%3D%20something"), "Important URL chars escaped");
+end
+
+function formencode(formencode)
+	assert_equal(formencode({ { name = "one", value = "1"}, { name = "two", value = "2" } }), "one=1&two=2", "Form encoded");
+	assert_equal(formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded");
+end
+
+function formdecode(formdecode)
+	local t = formdecode("one=1&two=2");
+	assert_table(t[1]);
+	assert_equal(t[1].name, "one"); assert_equal(t[1].value, "1");
+	assert_table(t[2]);
+	assert_equal(t[2].name, "two"); assert_equal(t[2].value, "2");
+
+	local t = formdecode("one+two=1&two+one%26=2");
+	assert_equal(t[1].name, "one two"); assert_equal(t[1].value, "1");
+	assert_equal(t[2].name, "two one&"); assert_equal(t[2].value, "2");
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_ip.lua	Wed May 22 14:33:18 2013 +0100
@@ -0,0 +1,89 @@
+
+function match(match, _M)
+	local _ = _M.new_ip;
+	local ip = _"10.20.30.40";
+	assert_equal(match(ip, _"10.0.0.0", 8), true);
+	assert_equal(match(ip, _"10.0.0.0", 16), false);
+	assert_equal(match(ip, _"10.0.0.0", 24), false);
+	assert_equal(match(ip, _"10.0.0.0", 32), false);
+
+	assert_equal(match(ip, _"10.20.0.0", 8), true);
+	assert_equal(match(ip, _"10.20.0.0", 16), true);
+	assert_equal(match(ip, _"10.20.0.0", 24), false);
+	assert_equal(match(ip, _"10.20.0.0", 32), false);
+
+	assert_equal(match(ip, _"0.0.0.0", 32), false);
+	assert_equal(match(ip, _"0.0.0.0", 0), true);
+	assert_equal(match(ip, _"0.0.0.0"), false);
+
+	assert_equal(match(ip, _"10.0.0.0", 255), false, "excessive number of bits");
+	assert_equal(match(ip, _"10.0.0.0", -8), true, "negative number of bits");
+	assert_equal(match(ip, _"10.0.0.0", -32), true, "negative number of bits");
+	assert_equal(match(ip, _"10.0.0.0", 0), true, "zero bits");
+	assert_equal(match(ip, _"10.0.0.0"), false, "no specified number of bits (differing ip)");
+	assert_equal(match(ip, _"10.20.30.40"), true, "no specified number of bits (same ip)");
+
+	assert_equal(match(_"127.0.0.1", _"127.0.0.1"), true, "simple ip");
+
+	assert_equal(match(_"8.8.8.8", _"8.8.0.0", 16), true);
+	assert_equal(match(_"8.8.4.4", _"8.8.0.0", 16), true);
+end
+
+function parse_cidr(parse_cidr, _M)
+	local new_ip = _M.new_ip;
+	
+	assert_equal(new_ip"0.0.0.0", new_ip"0.0.0.0")
+	
+	local function assert_cidr(cidr, ip, bits)
+		local parsed_ip, parsed_bits = parse_cidr(cidr);
+		assert_equal(new_ip(ip), parsed_ip, cidr.." parsed ip is "..ip);
+		assert_equal(bits, parsed_bits, cidr.." parsed bits is "..tostring(bits));
+	end
+	assert_cidr("0.0.0.0", "0.0.0.0", nil);
+	assert_cidr("127.0.0.1", "127.0.0.1", nil);
+	assert_cidr("127.0.0.1/0", "127.0.0.1", 0);
+	assert_cidr("127.0.0.1/8", "127.0.0.1", 8);
+	assert_cidr("127.0.0.1/32", "127.0.0.1", 32);
+	assert_cidr("127.0.0.1/256", "127.0.0.1", 256);
+	assert_cidr("::/48", "::", 48);
+end
+
+function new_ip(new_ip)
+	local v4, v6 = "IPv4", "IPv6";
+	local function assert_proto(s, proto)
+		local ip = new_ip(s);
+		if proto then
+			assert_equal(ip and ip.proto, proto, "protocol is correct for "..("%q"):format(s));
+		else
+			assert_equal(ip, nil, "address is invalid");
+		end
+	end
+	assert_proto("127.0.0.1", v4);
+	assert_proto("::1", v6);
+	assert_proto("", nil);
+	assert_proto("abc", nil);
+	assert_proto("   ", nil);
+end
+
+function commonPrefixLength(cpl, _M)
+	local new_ip = _M.new_ip;
+	local function assert_cpl6(a, b, len, v4)
+		local ipa, ipb = new_ip(a), new_ip(b);
+		if v4 then len = len+96; end
+		assert_equal(cpl(ipa, ipb), len, "common prefix length of "..a.." and "..b.." is "..len);
+		assert_equal(cpl(ipb, ipa), len, "common prefix length of "..b.." and "..a.." is "..len);
+	end
+	local function assert_cpl4(a, b, len)
+		return assert_cpl6(a, b, len, "IPv4");
+	end
+	assert_cpl4("0.0.0.0", "0.0.0.0", 32);
+	assert_cpl4("255.255.255.255", "0.0.0.0", 0);
+	assert_cpl4("255.255.255.255", "255.255.0.0", 16);
+	assert_cpl4("255.255.255.255", "255.255.255.255", 32);
+	assert_cpl4("255.255.255.255", "255.255.255.255", 32);
+
+	assert_cpl6("::1", "::1", 128);
+	assert_cpl6("abcd::1", "abcd::1", 128);
+	assert_cpl6("abcd::abcd", "abcd::", 112);
+	assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96);
+end
--- a/tests/test_util_rfc3484.lua	Wed May 22 14:32:02 2013 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
--- Prosody IM
--- Copyright (C) 2011 Florian Zeitz
---
--- This project is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-function source(source)
-	local new_ip = require"util.ip".new_ip;
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("3ffe::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, "3ffe::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "2001::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("ff05::1", "IPv6"), {new_ip("fe80::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::1", "prefer appropriate scope");
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::1", "IPv6"), new_ip("2002::1", "IPv6")}).addr, "2001::1", "prefer same address");
-	assert_equal(source(new_ip("fec0::1", "IPv6"), {new_ip("fec0::2", "IPv6"), new_ip("2001::1", "IPv6")}).addr, "fec0::2", "prefer appropriate scope");
-	assert_equal(source(new_ip("2001::1", "IPv6"), {new_ip("2001::2", "IPv6"), new_ip("3ffe::2", "IPv6")}).addr, "2001::2", "longest matching prefix");
-	assert_equal(source(new_ip("2002:836b:2179::1", "IPv6"), {new_ip("2002:836b:2179::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001::2", "IPv6")}).addr, "2002:836b:2179::d5e3:7953:13eb:22e8", "prefer matching label");
-end
-
-function destination(dest)
-	local order;
-	local new_ip = require"util.ip".new_ip;
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")})
-	assert_equal(order[1].addr, "2001::1", "prefer matching scope");
-	assert_equal(order[2].addr, "131.107.65.121", "prefer matching scope")
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("131.107.65.121", "IPv4")}, {new_ip("fe80::1", "IPv6"), new_ip("131.107.65.117", "IPv4")})
-	assert_equal(order[1].addr, "131.107.65.121", "prefer matching scope")
-	assert_equal(order[2].addr, "2001::1", "prefer matching scope")
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, {new_ip("2001::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")})
-	assert_equal(order[1].addr, "2001::1", "prefer higher precedence");
-	assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence");
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("fec0::1", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "fe80::1", "prefer smaller scope");
-	assert_equal(order[2].addr, "fec0::1", "prefer smaller scope");
-	assert_equal(order[3].addr, "2001::1", "prefer smaller scope");
-
-	order = dest({new_ip("2001::1", "IPv6"), new_ip("3ffe::1", "IPv6")}, {new_ip("2001::2", "IPv6"), new_ip("3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "2001::1", "longest matching prefix");
-	assert_equal(order[2].addr, "3ffe::1", "longest matching prefix");
-
-	order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "2002:836b:4179::1", "prefer matching label");
-	assert_equal(order[2].addr, "2001::1", "prefer matching label");
-
-	order = dest({new_ip("2002:836b:4179::1", "IPv6"), new_ip("2001::1", "IPv6")}, {new_ip("2002:836b:4179::2", "IPv6"), new_ip("2001::2", "IPv6"), new_ip("fe80::2", "IPv6")})
-	assert_equal(order[1].addr, "2001::1", "prefer higher precedence");
-	assert_equal(order[2].addr, "2002:836b:4179::1", "prefer higher precedence");
-end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_rfc6724.lua	Wed May 22 14:33:18 2013 +0100
@@ -0,0 +1,97 @@
+-- Prosody IM
+-- Copyright (C) 2011-2013 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+function source(source)
+	local new_ip = require"util.ip".new_ip;
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
+		"2001:db8:3::1",
+		"prefer appropriate scope");
+	assert_equal(source(new_ip("ff05::1", "IPv6"),
+			{new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr,
+		"2001:db8:3::1",
+		"prefer appropriate scope");
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr,
+		"2001:db8:1::1",
+		"prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now
+	assert_equal(source(new_ip("fe80::1", "IPv6"),
+			{new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr,
+		"fe80::2",
+		"prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
+		"2001:db8:1::2",
+		"longest matching prefix");
+--[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail
+	assert_equal(source(new_ip("2001:db8:1::1", "IPv6"),
+			{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr,
+		"2001:db8:3::2",
+		"prefer home address");
+]]
+	assert_equal(source(new_ip("2002:c633:6401::1", "IPv6"),
+			{new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr,
+		"2002:c633:6401::d5e3:7953:13eb:22e8",
+		"prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
+	assert_equal(source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"),
+			{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr,
+		"2001:db8:1::d5e3:7953:13eb:22e8",
+		"prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now
+end
+
+function destination(dest)
+	local order;
+	local new_ip = require"util.ip".new_ip;
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer matching scope");
+	assert_equal(order[2].addr, "198.51.100.121", "prefer matching scope");
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")},
+		{new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")})
+	assert_equal(order[1].addr, "198.51.100.121", "prefer matching scope");
+	assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching scope");
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
+	assert_equal(order[2].addr, "10.1.2.3", "prefer higher precedence");
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "fe80::1", "prefer smaller scope");
+	assert_equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope");
+
+--[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer home address");
+	assert_equal(order[2].addr, "fe80::1", "prefer home address");
+]]
+
+--[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses");
+	assert_equal(order[2].addr, "fe80::1", "avoid deprecated addresses");
+]]
+
+	order = dest({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")},
+		{new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "longest matching prefix");
+	assert_equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix");
+
+	order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
+		{new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2002:c633:6401::1", "prefer matching label");
+	assert_equal(order[2].addr, "2001:db8:1::1", "prefer matching label");
+
+	order = dest({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")},
+		{new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")})
+	assert_equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence");
+	assert_equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence");
+end
--- a/util/ip.lua	Wed May 22 14:32:02 2013 +0100
+++ b/util/ip.lua	Wed May 22 14:33:18 2013 +0100
@@ -12,7 +12,17 @@
 local hex2bits = { ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", ["8"] = "1000", ["9"] = "1001", ["A"] = "1010", ["B"] = "1011", ["C"] = "1100", ["D"] = "1101", ["E"] = "1110", ["F"] = "1111" };
 
 local function new_ip(ipStr, proto)
-	if proto ~= "IPv4" and proto ~= "IPv6" then
+	if not proto then
+		local sep = ipStr:match("^%x+(.)");
+		if sep == ":" or (not(sep) and ipStr:sub(1,1) == ":") then
+			proto = "IPv6"
+		elseif sep == "." then
+			proto = "IPv4"
+		end
+		if not proto then
+			return nil, "invalid address";
+		end
+	elseif proto ~= "IPv4" and proto ~= "IPv6" then
 		return nil, "invalid protocol";
 	end
 	if proto == "IPv6" and ipStr:find('.', 1, true) then
@@ -192,5 +202,43 @@
 	return value;
 end
 
+function ip_methods:private()
+	local private = self.scope ~= 0xE;
+	if not private and self.proto == "IPv4" then
+		local ip = self.addr;
+		local fields = {};
+		ip:gsub("([^.]*).?", function (c) fields[#fields + 1] = tonumber(c) end);
+		if fields[1] == 127 or fields[1] == 10 or (fields[1] == 192 and fields[2] == 168)
+		or (fields[1] == 172 and (fields[2] >= 16 or fields[2] <= 32)) then
+			private = true;
+		end
+	end
+	self.private = private;
+	return private;
+end
+
+local function parse_cidr(cidr)
+	local bits;
+	local ip_len = cidr:find("/", 1, true);
+	if ip_len then
+		bits = tonumber(cidr:sub(ip_len+1, -1));
+		cidr = cidr:sub(1, ip_len-1);
+	end
+	return new_ip(cidr), bits;
+end
+
+local function match(ipA, ipB, bits)
+	local common_bits = commonPrefixLength(ipA, ipB);
+	if not bits then
+		return ipA == ipB;
+	end
+	if bits and ipB.proto == "IPv4" then
+		common_bits = common_bits - 96; -- v6 mapped addresses always share these bits
+	end
+	return common_bits >= bits;
+end
+
 return {new_ip = new_ip,
-	commonPrefixLength = commonPrefixLength};
+	commonPrefixLength = commonPrefixLength,
+	parse_cidr = parse_cidr,
+	match=match};
--- a/util/iterators.lua	Wed May 22 14:32:02 2013 +0100
+++ b/util/iterators.lua	Wed May 22 14:33:18 2013 +0100
@@ -10,6 +10,10 @@
 
 local it = {};
 
+local t_insert = table.insert;
+local select, unpack, next = select, unpack, next;
+local function pack(...) return { n = select("#", ...), ... }; end
+
 -- Reverse an iterator
 function it.reverse(f, s, var)
 	local results = {};
@@ -19,7 +23,7 @@
 		local ret = { f(s, var) };
 		var = ret[1];
 	        if var == nil then break; end
-		table.insert(results, 1, ret);
+		t_insert(results, 1, ret);
 	end
 	
 	-- Then return our reverse one
@@ -55,12 +59,12 @@
 	
 	return function ()
 		while true do
-			local ret = { f(s, var) };
+			local ret = pack(f(s, var));
 			var = ret[1];
 		        if var == nil then break; end
 		        if not set[var] then
 				set[var] = true;
-				return var;
+				return unpack(ret, 1, ret.n);
 			end
 		end
 	end;
@@ -71,8 +75,7 @@
 	local x = 0;
 	
 	while true do
-		local ret = { f(s, var) };
-		var = ret[1];
+		var = f(s, var);
 	        if var == nil then break; end
 		x = x + 1;
 	end
@@ -104,7 +107,7 @@
 function it.tail(n, f, s, var)
 	local results, count = {}, 0;
 	while true do
-		local ret = { f(s, var) };
+		local ret = pack(f(s, var));
 		var = ret[1];
 	        if var == nil then break; end
 		results[(count%n)+1] = ret;
@@ -117,9 +120,24 @@
 	return function ()
 		pos = pos + 1;
 		if pos > n then return nil; end
-		return unpack(results[((count-1+pos)%n)+1]);
+		local ret = results[((count-1+pos)%n)+1];
+		return unpack(ret, 1, ret.n);
 	end
-	--return reverse(head(n, reverse(f, s, var)));
+	--return reverse(head(n, reverse(f, s, var))); -- !
+end
+
+function it.filter(filter, f, s, var)
+	if type(filter) ~= "function" then
+		local filter_value = filter;
+		function filter(x) return x ~= filter_value; end
+	end
+	return function (s, var)
+		local ret;
+		repeat ret = pack(f(s, var));
+			var = ret[1];
+		until var == nil or filter(unpack(ret, 1, ret.n));
+		return unpack(ret, 1, ret.n);
+	end, s, var;
 end
 
 local function _ripairs_iter(t, key) if key > 1 then return key-1, t[key-1]; end end
@@ -139,7 +157,7 @@
 	while true do
 		var = f(s, var);
 	        if var == nil then break; end
-		table.insert(t, var);
+		t_insert(t, var);
 	end
 	return t;
 end