Merge 0.9->0.10
authorMatthew Wild <mwild1@gmail.com>
Tue, 24 Feb 2015 15:22:09 +0000
changeset 6583 74946f050209
parent 6580 1e749832ed3b (diff)
parent 6582 5a82ee60e07e (current diff)
child 6584 f2a7ad099e01
child 6585 71b5de1d830b
Merge 0.9->0.10
configure
plugins/mod_admin_telnet.lua
--- a/Makefile	Tue Feb 24 15:19:20 2015 +0000
+++ b/Makefile	Tue Feb 24 15:22:09 2015 +0000
@@ -31,8 +31,9 @@
 	install -m755 ./prosodyctl.install $(BIN)/prosodyctl
 	install -m644 core/* $(SOURCE)/core
 	install -m644 net/*.lua $(SOURCE)/net
-	install -d $(SOURCE)/net/http
+	install -d $(SOURCE)/net/http $(SOURCE)/net/websocket
 	install -m644 net/http/*.lua $(SOURCE)/net/http
+	install -m644 net/websocket/*.lua $(SOURCE)/net/websocket
 	install -m644 util/*.lua $(SOURCE)/util
 	install -m644 util/*.so $(SOURCE)/util
 	install -d $(SOURCE)/util/sasl
--- a/configure	Tue Feb 24 15:19:20 2015 +0000
+++ b/configure	Tue Feb 24 15:22:09 2015 +0000
@@ -99,32 +99,31 @@
    --ostype=*)
       OSTYPE="$value"
       OSTYPE_SET=yes
-      if [ "$OSTYPE" = "debian" ]
-      then LUA_SUFFIX="5.1";
-	LUA_SUFFIX_SET=yes
-	RUNWITH="lua5.1"
-	LUA_INCDIR=/usr/include/lua5.1;
-	LUA_INCDIR_SET=yes
-	CFLAGS="$CFLAGS -D_GNU_SOURCE"
-	fi
-	if [ "$OSTYPE" = "macosx" ]
-	then LUA_INCDIR=/usr/local/include;
-	LUA_INCDIR_SET=yes
-	LUA_LIBDIR=/usr/local/lib
-	LUA_LIBDIR_SET=yes
-	LDFLAGS="-bundle -undefined dynamic_lookup"
-	fi
-        if [ "$OSTYPE" = "linux" ]
-        then LUA_INCDIR=/usr/local/include;
+      if [ "$OSTYPE" = "debian" ]; then
+        LUA_SUFFIX="5.1";
+      	LUA_SUFFIX_SET=yes
+      	RUNWITH="lua5.1"
+      	LUA_INCDIR=/usr/include/lua5.1;
+      	LUA_INCDIR_SET=yes
+      	CFLAGS="$CFLAGS -D_GNU_SOURCE"
+    	fi
+    	if [ "$OSTYPE" = "macosx" ]; then
+        LUA_INCDIR=/usr/local/include;
+      	LUA_INCDIR_SET=yes
+      	LUA_LIBDIR=/usr/local/lib
+      	LUA_LIBDIR_SET=yes
+      	LDFLAGS="-bundle -undefined dynamic_lookup"
+    	fi
+      if [ "$OSTYPE" = "linux" ]; then
+        LUA_INCDIR=/usr/local/include;
         LUA_INCDIR_SET=yes
         LUA_LIBDIR=/usr/local/lib
         LUA_LIBDIR_SET=yes
-        CFLAGS="-Wall -fPIC"
-        CFLAGS="$CFLAGS -D_GNU_SOURCE"
+        CFLAGS="-Wall -fPIC -D_GNU_SOURCE"
         LDFLAGS="-shared"
-        fi
-        if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ]
-        then LUA_INCDIR="/usr/local/include/lua51"
+      fi
+      if [ "$OSTYPE" = "freebsd" -o "$OSTYPE" = "openbsd" ]; then
+        LUA_INCDIR="/usr/local/include/lua51"
         LUA_INCDIR_SET=yes
         CFLAGS="-Wall -fPIC -I/usr/local/include"
         LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared"
@@ -132,10 +131,10 @@
         LUA_SUFFIX_SET=yes
         LUA_DIR=/usr/local
         LUA_DIR_SET=yes
-        fi
-        if [ "$OSTYPE" = "openbsd" ]
-        then LUA_INCDIR="/usr/local/include";
-        fi
+      fi
+      if [ "$OSTYPE" = "openbsd" ]; then
+        LUA_INCDIR="/usr/local/include";
+      fi
       ;;
    --libdir=*)
       LIBDIR="$value"
@@ -303,7 +302,7 @@
 	IDNA_LIBS="$ICU_FLAGS"
 	CFLAGS="$CFLAGS -DUSE_STRINGPREP_ICU"
 fi
-if [ "$IDN_LIBRARY" = "idn" ] 
+if [ "$IDN_LIBRARY" = "idn" ]
 then
 	IDNA_LIBS="-l$IDN_LIB"
 fi
--- a/core/certmanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/certmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,97 +1,127 @@
 -- 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 softreq = require"util.dependencies".softreq;
+local ssl = softreq"ssl";
+if not ssl then
+	return {
+		create_context = function ()
+			return nil, "LuaSec (required for encryption) was not found";
+		end;
+		reload_ssl_config = function () end;
+	}
+end
+
 local configmanager = require "core.configmanager";
 local log = require "util.logger".init("certmanager");
-local ssl = ssl;
-local ssl_newcontext = ssl and ssl.newcontext;
+local ssl_context = ssl.context or softreq"ssl.context";
+local ssl_x509 = ssl.x509 or softreq"ssl.x509";
+local ssl_newcontext = ssl.newcontext;
+local new_config = require"util.sslconfig".new;
 
 local tostring = tostring;
+local pairs = pairs;
 local type = type;
 local io_open = io.open;
+local select = select;
 
 local prosody = prosody;
-local resolve_path = configmanager.resolve_relative_path;
+local resolve_path = require"util.paths".resolve_relative_path;
 local config_path = prosody.paths.config;
 
-local luasec_has_noticket, luasec_has_verifyext, luasec_has_no_compression;
-if ssl then
-	local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)");
-	luasec_has_noticket = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=4;
-	luasec_has_verifyext = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5;
-	luasec_has_no_compression = tonumber(luasec_major)>0 or tonumber(luasec_minor)>=5;
-end
+local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)");
+local luasec_version = luasec_major * 100 + luasec_minor;
+local luasec_has = {
+	-- TODO If LuaSec ever starts exposing these things itself, use that instead
+	cipher_server_preference = luasec_version >= 2;
+	no_ticket = luasec_version >= 4;
+	no_compression = luasec_version >= 5;
+	single_dh_use = luasec_version >= 2;
+	single_ecdh_use = luasec_version >= 2;
+};
 
 module "certmanager"
 
 -- Global SSL options if not overridden per-host
-local default_ssl_config = configmanager.get("*", "ssl");
-local default_capath = "/etc/ssl/certs";
-local default_verify = (ssl and ssl.x509 and { "peer", "client_once", }) or "none";
-local default_options = { "no_sslv2", "no_sslv3", "cipher_server_preference", luasec_has_noticket and "no_ticket" or nil };
-local default_verifyext = { "lsec_continue", "lsec_ignore_purpose" };
+local global_ssl_config = configmanager.get("*", "ssl");
 
-if ssl and not luasec_has_verifyext and ssl.x509 then
+-- Built-in defaults
+local core_defaults = {
+	capath = "/etc/ssl/certs";
+	depth = 9;
+	protocol = "tlsv1+";
+	verify = (ssl_x509 and { "peer", "client_once", }) or "none";
+	options = {
+		cipher_server_preference = luasec_has.cipher_server_preference;
+		no_ticket = luasec_has.no_ticket;
+		no_compression = luasec_has.no_compression and configmanager.get("*", "ssl_compression") ~= true;
+		single_dh_use = luasec_has.single_dh_use;
+		single_ecdh_use = luasec_has.single_ecdh_use;
+	};
+	verifyext = { "lsec_continue", "lsec_ignore_purpose" };
+	curve = "secp384r1";
+	ciphers = "HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL";
+}
+local path_options = { -- These we pass through resolve_path()
+	key = true, certificate = true, cafile = true, capath = true, dhparam = true
+}
+
+if luasec_version < 5 and ssl_x509 then
 	-- COMPAT mw/luasec-hg
-	for i=1,#default_verifyext do -- Remove lsec_ prefix
-		default_verify[#default_verify+1] = default_verifyext[i]:sub(6);
+	for i=1,#core_defaults.verifyext do -- Remove lsec_ prefix
+		core_defaults.verify[#core_defaults.verify+1] = core_defaults.verifyext[i]:sub(6);
 	end
 end
-if luasec_has_no_compression and configmanager.get("*", "ssl_compression") ~= true then
-	default_options[#default_options+1] = "no_compression";
-end
-
-if luasec_has_no_compression then -- Has no_compression? Then it has these too...
-	default_options[#default_options+1] = "single_dh_use";
-	default_options[#default_options+1] = "single_ecdh_use";
-end
 
-function create_context(host, mode, user_ssl_config)
-	user_ssl_config = user_ssl_config or default_ssl_config;
+function create_context(host, mode, ...)
+	local cfg = new_config();
+	cfg:apply(core_defaults);
+	cfg:apply(global_ssl_config);
+	cfg:apply({
+		mode = mode,
+		-- We can't read the password interactively when daemonized
+		password = function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end;
+	});
 
-	if not ssl then return nil, "LuaSec (required for encryption) was not found"; end
-	if not user_ssl_config then return nil, "No SSL/TLS configuration present for "..host; end
-	
-	local ssl_config = {
-		mode = mode;
-		protocol = user_ssl_config.protocol or "sslv23";
-		key = resolve_path(config_path, user_ssl_config.key);
-		password = user_ssl_config.password or function() log("error", "Encrypted certificate for %s requires 'ssl' 'password' to be set in config", host); end;
-		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 default_verify;
-		verifyext = user_ssl_config.verifyext or default_verifyext;
-		options = user_ssl_config.options or default_options;
-		depth = user_ssl_config.depth;
-		curve = user_ssl_config.curve or "secp384r1";
-		ciphers = user_ssl_config.ciphers or "HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL";
-		dhparam = user_ssl_config.dhparam;
-	};
+	for i = select('#', ...), 1, -1 do
+		cfg:apply(select(i, ...));
+	end
+	local user_ssl_config = cfg:final();
+
+	if mode == "server" then
+		if not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end
+		if not user_ssl_config.certificate then return nil, "No certificate present in SSL/TLS configuration for "..host; end
+	end
+
+	for option in pairs(path_options) do
+		if type(user_ssl_config[option]) == "string" then
+			user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]);
+		end
+	end
 
 	-- LuaSec expects dhparam to be a callback that takes two arguments.
 	-- We ignore those because it is mostly used for having a separate
 	-- set of params for EXPORT ciphers, which we don't have by default.
-	if type(ssl_config.dhparam) == "string" then
-		local f, err = io_open(resolve_path(config_path, ssl_config.dhparam));
+	if type(user_ssl_config.dhparam) == "string" then
+		local f, err = io_open(user_ssl_config.dhparam);
 		if not f then return nil, "Could not open DH parameters: "..err end
 		local dhparam = f:read("*a");
 		f:close();
-		ssl_config.dhparam = function() return dhparam; end
+		user_ssl_config.dhparam = function() return dhparam; end
 	end
 
-	local ctx, err = ssl_newcontext(ssl_config);
+	local ctx, err = ssl_newcontext(user_ssl_config);
 
-	-- COMPAT: LuaSec 0.4.1 ignores the cipher list from the config, so we have to take
-	-- care of it ourselves...
-	if ctx and ssl_config.ciphers then
+	-- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care
+	-- of it ourselves (W/A for #x)
+	if ctx and user_ssl_config.ciphers then
 		local success;
-		success, err = ssl.context.setcipher(ctx, ssl_config.ciphers);
+		success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers);
 		if not success then ctx = nil; end
 	end
 
@@ -100,9 +130,9 @@
 		local file = err:match("^error loading (.-) %(");
 		if file then
 			if file == "private key" then
-				file = ssl_config.key or "your private key";
+				file = user_ssl_config.key or "your private key";
 			elseif file == "certificate" then
-				file = ssl_config.certificate or "your certificate file";
+				file = user_ssl_config.certificate or "your certificate file";
 			end
 			local reason = err:match("%((.+)%)$") or "some reason";
 			if reason == "Permission denied" then
@@ -121,11 +151,14 @@
 			log("error", "SSL/TLS: Error initialising for %s: %s", host, err);
 		end
 	end
-	return ctx, err;
+	return ctx, err, user_ssl_config;
 end
 
 function reload_ssl_config()
-	default_ssl_config = configmanager.get("*", "ssl");
+	global_ssl_config = configmanager.get("*", "ssl");
+	if luasec_has.no_compression then
+		core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true;
+	end
 end
 
 prosody.events.add_handler("config-reloaded", reload_ssl_config);
--- a/core/configmanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/configmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -15,6 +15,8 @@
 
 local envload = require"util.envload".envload;
 local deps = require"util.dependencies";
+local resolve_relative_path = require"util.paths".resolve_relative_path;
+local glob_to_pattern = require"util.paths".glob_to_pattern;
 local path_sep = package.config:sub(1,1);
 
 local have_encodings, encodings = pcall(require, "util.encodings");
@@ -22,6 +24,8 @@
 
 module "configmanager"
 
+_M.resolve_relative_path = resolve_relative_path; -- COMPAT
+
 local parsers = {};
 
 local config_mt = { __index = function (t, k) return rawget(t, "*"); end};
@@ -69,41 +73,6 @@
 	return set(config, host, key, value);
 end
 
--- Helper function to resolve relative paths (needed by config)
-do
-	function resolve_relative_path(parent_path, path)
-		if path then
-			-- Some normalization
-			parent_path = parent_path:gsub("%"..path_sep.."+$", "");
-			path = path:gsub("^%.%"..path_sep.."+", "");
-			
-			local is_relative;
-			if path_sep == "/" and path:sub(1,1) ~= "/" then
-				is_relative = true;
-			elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" and path:sub(2,3) ~= ":/")) then
-				is_relative = true;
-			end
-			if is_relative then
-				return parent_path..path_sep..path;
-			end
-		end
-		return path;
-	end	
-end
-
--- Helper function to convert a glob to a Lua pattern
-local function glob_to_pattern(glob)
-	return "^"..glob:gsub("[%p*?]", function (c)
-		if c == "*" then
-			return ".*";
-		elseif c == "?" then
-			return ".";
-		else
-			return "%"..c;
-		end
-	end).."$";
-end
-
 function load(filename, format)
 	format = format or filename:match("%w+$");
 
@@ -170,7 +139,7 @@
 					set(config, env.__currenthost or "*", k, v);
 				end
 		});
-		
+
 		rawset(env, "__currenthost", "*") -- Default is global
 		function env.VirtualHost(name)
 			name = nameprep(name);
@@ -189,7 +158,7 @@
 			end;
 		end
 		env.Host, env.host = env.VirtualHost, env.VirtualHost;
-		
+
 		function env.Component(name)
 			name = nameprep(name);
 			if rawget(config, name) and rawget(config[name], "defined") and not rawget(config[name], "component_module") then
@@ -206,7 +175,7 @@
 					set(config, name or "*", option_name, option_value);
 				end
 			end
-	
+
 			return function (module)
 					if type(module) == "string" then
 						set(config, name, "component_module", module);
@@ -216,7 +185,7 @@
 				end
 		end
 		env.component = env.Component;
-		
+
 		function env.Include(file)
 			if file:match("[*?]") then
 				local lfs = deps.softreq "lfs";
@@ -249,26 +218,26 @@
 			end
 		end
 		env.include = env.Include;
-		
+
 		function env.RunScript(file)
 			return dofile(resolve_relative_path(config_file:gsub("[^"..path_sep.."]+$", ""), file));
 		end
-		
+
 		local chunk, err = envload(data, "@"..config_file, env);
-		
+
 		if not chunk then
 			return nil, err;
 		end
-		
+
 		local ok, err = pcall(chunk);
-		
+
 		if not ok then
 			return nil, err;
 		end
-		
+
 		return true;
 	end
-	
+
 end
 
 return _M;
--- a/core/hostmanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/hostmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -13,7 +13,6 @@
 local NULL = {};
 
 local jid_split = require "util.jid".split;
-local uuid_gen = require "util.uuid".generate;
 
 local log = require "util.logger".init("hostmanager");
 
@@ -35,7 +34,7 @@
 local function load_enabled_hosts(config)
 	local defined_hosts = config or configmanager.getconfig();
 	local activated_any_host;
-	
+
 	for host, host_config in pairs(defined_hosts) do
 		if host ~= "*" and host_config.enabled ~= false then
 			if not host_config.component_module then
@@ -44,11 +43,11 @@
 			activate(host, host_config);
 		end
 	end
-	
+
 	if not activated_any_host then
 		log("error", "No active VirtualHost entries in the config file. This may cause unexpected behaviour as no modules will be loaded.");
 	end
-	
+
 	prosody_events.fire_event("hosts-activated", defined_hosts);
 	hosts_loaded_once = true;
 end
@@ -74,7 +73,6 @@
 		host = host;
 		s2sout = {};
 		events = events_new();
-		dialback_secret = configmanager.get(host, "dialback_secret") or uuid_gen();
 		send = host_send;
 		modules = {};
 	};
@@ -93,7 +91,7 @@
 			log("warn", "%s: Option '%s' has no effect for virtual hosts - put it in the server-wide section instead", host, option_name);
 		end
 	end
-	
+
 	log((hosts_loaded_once and "info") or "debug", "Activated host: %s", host);
 	prosody_events.fire_event("host-activated", host);
 	return true;
@@ -104,11 +102,11 @@
 	if not host_session then return nil, "The host "..tostring(host).." is not activated"; end
 	log("info", "Deactivating host: %s", host);
 	prosody_events.fire_event("host-deactivating", { host = host, host_session = host_session, reason = reason });
-	
+
 	if type(reason) ~= "table" then
 		reason = { condition = "host-gone", text = tostring(reason or "This server has stopped serving "..host) };
 	end
-	
+
 	-- Disconnect local users, s2s connections
 	-- TODO: These should move to mod_c2s and mod_s2s (how do they know they're being unloaded and not reloaded?)
 	if host_session.sessions then
--- a/core/loggingmanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/loggingmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -48,7 +48,7 @@
 	if sink_maker then
 		-- Create sink
 		local sink = sink_maker(sink_config);
-		
+
 		-- Set sink for all chosen levels
 		for level in pairs(get_levels(sink_config.levels or logging_levels)) do
 			logger.add_level_sink(level, sink);
@@ -63,7 +63,7 @@
 -- the log_sink_types table.
 function apply_sink_rules(sink_type)
 	if type(logging_config) == "table" then
-		
+
 		for _, level in ipairs(logging_levels) do
 			if type(logging_config[level]) == "string" then
 				local value = logging_config[level];
@@ -82,7 +82,7 @@
 				end
 			end
 		end
-		
+
 		for _, sink_config in ipairs(logging_config) do
 			if (type(sink_config) == "table" and sink_config.to == sink_type) then
 				add_rule(sink_config);
@@ -128,7 +128,7 @@
 			end
 		end
 	end
-	
+
 	for _, level in ipairs(criteria) do
 		set[level] = true;
 	end
@@ -138,12 +138,12 @@
 -- Initialize config, etc. --
 function reload_logging()
 	local old_sink_types = {};
-	
+
 	for name, sink_maker in pairs(log_sink_types) do
 		old_sink_types[name] = sink_maker;
 		log_sink_types[name] = nil;
 	end
-	
+
 	logger.reset();
 
 	local debug_mode = config.get("*", "debug");
@@ -155,12 +155,12 @@
 	default_timestamp = "%b %d %H:%M:%S";
 
 	logging_config = config.get("*", "log") or default_logging;
-	
-	
+
+
 	for name, sink_maker in pairs(old_sink_types) do
 		log_sink_types[name] = sink_maker;
 	end
-	
+
 	prosody.events.fire_event("logging-reloaded");
 end
 
@@ -179,11 +179,11 @@
 
 function log_sink_types.stdout(config)
 	local timestamps = config.timestamps;
-	
+
 	if timestamps == true then
 		timestamps = default_timestamp; -- Default format
 	end
-	
+
 	return function (name, level, message, ...)
 		sourcewidth = math_max(#name+2, sourcewidth);
 		local namelen = #name;
@@ -200,7 +200,7 @@
 
 do
 	local do_pretty_printing = true;
-	
+
 	local logstyles = {};
 	if do_pretty_printing then
 		logstyles["info"] = getstyle("bold");
@@ -212,7 +212,7 @@
 		if not do_pretty_printing then
 			return log_sink_types.stdout(config);
 		end
-		
+
 		local timestamps = config.timestamps;
 
 		if timestamps == true then
@@ -222,7 +222,7 @@
 		return function (name, level, message, ...)
 			sourcewidth = math_max(#name+2, sourcewidth);
 			local namelen = #name;
-			
+
 			if timestamps then
 				io_write(os_date(timestamps), " ");
 			end
--- a/core/moduleapi.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/moduleapi.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,23 +1,26 @@
 -- Prosody IM
 -- Copyright (C) 2008-2012 Matthew Wild
 -- Copyright (C) 2008-2012 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 modulemanager = require "modulemanager"; -- This is necessary to avoid require loops
+local modulemanager; -- This gets set from modulemanager
 local array = require "util.array";
 local set = require "util.set";
 local logger = require "util.logger";
 local pluginloader = require "util.pluginloader";
 local timer = require "util.timer";
+local resolve_relative_path = require"util.paths".resolve_relative_path;
+local measure = require "core.statsmanager".measure;
 
 local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
 local error, setmetatable, type = error, setmetatable, type;
 local ipairs, pairs, select, unpack = ipairs, pairs, select, unpack;
 local tonumber, tostring = tonumber, tostring;
+local require = require;
 
 local prosody = prosody;
 local hosts = prosody.hosts;
@@ -44,7 +47,7 @@
 end
 
 function api:get_host_type()
-	return self.host ~= "*" and hosts[self.host].type or nil;
+	return (self.host == "*" and "global") or hosts[self.host].type or "local";
 end
 
 function api:set_global()
@@ -74,7 +77,7 @@
 function api:has_identity(category, type, name)
 	for _, id in ipairs(self:get_host_items("identity")) do
 		if id.category == category and id.type == type and id.name == name then
-			return true; 
+			return true;
 		end
 	end
 	return false;
@@ -113,6 +116,10 @@
 end
 api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9
 
+function api:unhook(event, handler)
+	return self:unhook_object_event((hosts[self.host] or prosody).events, event, handler);
+end
+
 function api:require(lib)
 	local f, n = pluginloader.load_code(self.name, lib..".lib.lua", self.environment);
 	if not f then
@@ -252,21 +259,21 @@
 	if value == nil then
 		return nil;
 	end
-	
+
 	if type(value) ~= "table" then
 		return array{ value }; -- Assume any non-list is a single-item list
 	end
-	
+
 	return array():append(value); -- Clone
 end
 
 function api:get_option_set(name, ...)
 	local value = self:get_option_array(name, ...);
-	
+
 	if value == nil then
 		return nil;
 	end
-	
+
 	return set.new(value);
 end
 
@@ -356,12 +363,21 @@
 end
 
 function api:load_resource(path, mode)
-	path = config.resolve_relative_path(self:get_directory(), path);
+	path = resolve_relative_path(self:get_directory(), path);
 	return io.open(path, mode);
 end
 
 function api:open_store(name, type)
-	return storagemanager.open(self.host, name or self.name, type);
+	return require"core.storagemanager".open(self.host, name or self.name, type);
+end
+
+function api:measure(name, type)
+	return measure(type, "/"..self.host.."/mod_"..self.name.."/"..name);
+end
+
+function api.init(mm)
+	modulemanager = mm;
+	return api;
 end
 
 return api;
--- a/core/modulemanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/modulemanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -29,7 +29,7 @@
 	return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end);
 end
 
-local autoload_modules = {"presence", "message", "iq", "offline", "c2s", "s2s"};
+local autoload_modules = {prosody.platform, "presence", "message", "iq", "offline", "c2s", "s2s", "s2s_auth_certs"};
 local component_inheritable_modules = {"tls", "saslauth", "dialback", "iq", "s2s"};
 
 -- We need this to let modules access the real global namespace
@@ -37,7 +37,7 @@
 
 module "modulemanager"
 
-local api = _G.require "core.moduleapi"; -- Module API container
+local api = _G.require "core.moduleapi".init(_M); -- Module API container
 
 -- [host] = { [module] = module_env }
 local modulemap = { ["*"] = {} };
@@ -45,28 +45,28 @@
 -- Load modules when a host is activated
 function load_modules_for_host(host)
 	local component = config.get(host, "component_module");
-	
+
 	local global_modules_enabled = config.get("*", "modules_enabled");
 	local global_modules_disabled = config.get("*", "modules_disabled");
 	local host_modules_enabled = config.get(host, "modules_enabled");
 	local host_modules_disabled = config.get(host, "modules_disabled");
-	
+
 	if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end
 	if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end
-	
+
 	local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled);
 	if component then
 		global_modules = set.intersection(set.new(component_inheritable_modules), global_modules);
 	end
 	local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled);
-	
+
 	-- COMPAT w/ pre 0.8
 	if modules:contains("console") then
 		log("error", "The mod_console plugin has been renamed to mod_admin_telnet. Please update your config.");
 		modules:remove("console");
 		modules:add("admin_telnet");
 	end
-	
+
 	if component then
 		load(host, component);
 	end
@@ -84,18 +84,18 @@
 local function do_unload_module(host, name)
 	local mod = get_module(host, name);
 	if not mod then return nil, "module-not-loaded"; end
-	
+
 	if module_has_method(mod, "unload") then
 		local ok, err = call_module_method(mod, "unload");
 		if (not ok) and err then
 			log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
 		end
 	end
-	
+
 	for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do
 		object.remove_handler(event, handler);
 	end
-	
+
 	if mod.module.items then -- remove items
 		local events = (host == "*" and prosody.events) or hosts[host].events;
 		for key,t in pairs(mod.module.items) do
@@ -117,11 +117,11 @@
 	elseif not hosts[host] and host ~= "*"then
 		return nil, "unknown-host";
 	end
-	
+
 	if not modulemap[host] then
 		modulemap[host] = hosts[host].modules;
 	end
-	
+
 	if modulemap[host][module_name] then
 		log("debug", "%s is already loaded for %s, so not loading again", module_name, host);
 		return nil, "module-already-loaded";
@@ -147,7 +147,7 @@
 		end
 		return nil, "global-module-already-loaded";
 	end
-	
+
 
 
 	local _log = logger.init(host..":"..module_name);
@@ -158,7 +158,7 @@
 
 	local pluginenv = setmetatable({ module = api_instance }, { __index = _G });
 	api_instance.environment = pluginenv;
-	
+
 	local mod, err = pluginloader.load_code(module_name, nil, pluginenv);
 	if not mod then
 		log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
--- a/core/portmanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/portmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -9,7 +9,7 @@
 
 local table = table;
 local setmetatable, rawset, rawget = setmetatable, rawset, rawget;
-local type, tonumber, tostring, ipairs, pairs = type, tonumber, tostring, ipairs, pairs;
+local type, tonumber, tostring, ipairs = type, tonumber, tostring, ipairs;
 
 local prosody = prosody;
 local fire_event = prosody.events.fire_event;
@@ -72,16 +72,6 @@
 	unregister_service(item.name, item);
 end);
 
-local function duplicate_ssl_config(ssl_config)
-	local ssl_config = type(ssl_config) == "table" and ssl_config or {};
-
-	local _config = {};
-	for k, v in pairs(ssl_config) do
-		_config[k] = v;
-	end
-	return _config;
-end
-
 --- Public API
 
 function activate(service_name)
@@ -89,7 +79,7 @@
 	if not service_info then
 		return nil, "Unknown service: "..service_name;
 	end
-	
+
 	local listener = service_info.listener;
 
 	local config_prefix = (service_info.config_prefix or service_name).."_";
@@ -105,7 +95,7 @@
 		or listener.default_interface -- COMPAT w/pre0.9
 		or default_interfaces
 	bind_interfaces = set.new(type(bind_interfaces)~="table" and {bind_interfaces} or bind_interfaces);
-	
+
 	local bind_ports = config.get("*", config_prefix.."ports")
 		or service_info.default_ports
 		or {service_info.default_port
@@ -115,7 +105,7 @@
 
 	local mode, ssl = listener.default_mode or default_mode;
 	local hooked_ports = {};
-	
+
 	for interface in bind_interfaces do
 		for port in bind_ports do
 			local port_number = tonumber(port);
@@ -127,24 +117,15 @@
 				local err;
 				-- Create SSL context for this service/port
 				if service_info.encryption == "ssl" then
-					local ssl_config = duplicate_ssl_config((config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[interface])
-								or (config.get("*", config_prefix.."ssl") and config.get("*", config_prefix.."ssl")[port])
-								or config.get("*", config_prefix.."ssl")
-								or (config.get("*", "ssl") and config.get("*", "ssl")[interface])
-								or (config.get("*", "ssl") and config.get("*", "ssl")[port])
-								or config.get("*", "ssl"));
-					-- add default entries for, or override ssl configuration
-					if ssl_config and service_info.ssl_config then
-						for key, value in pairs(service_info.ssl_config) do
-							if not service_info.ssl_config_override and not ssl_config[key] then
-								ssl_config[key] = value;
-							elseif service_info.ssl_config_override then
-								ssl_config[key] = value;
-							end
-						end
-					end
-
-					ssl, err = certmanager.create_context(service_info.name.." port "..port, "server", ssl_config);
+					local global_ssl_config = config.get("*", "ssl") or {};
+					local prefix_ssl_config = config.get("*", config_prefix.."ssl") or global_ssl_config;
+					ssl, err = certmanager.create_context(service_info.name.." port "..port, "server",
+						service_info.ssl_config or {},
+						prefix_ssl_config[interface],
+						prefix_ssl_config[port],
+						prefix_ssl_config,
+						global_ssl_config[interface],
+						global_ssl_config[port]);
 					if not ssl then
 						log("error", "Error binding encrypted port for %s: %s", service_info.name, error_to_friendly_message(service_name, port_number, err) or "unknown error");
 					end
@@ -190,7 +171,7 @@
 			log("error", "Failed to activate service '%s': %s", service_name, err or "unknown error");
 		end
 	end
-	
+
 	fire_event("service-added", { name = service_name, service = service_info });
 	return true;
 end
--- a/core/rostermanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/rostermanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -15,7 +15,7 @@
 local tostring = tostring;
 
 local hosts = hosts;
-local bare_sessions = bare_sessions;
+local bare_sessions = prosody.bare_sessions;
 
 local datamanager = require "util.datamanager"
 local um_user_exists = require "core.usermanager".user_exists;
@@ -100,7 +100,7 @@
 		log("warn", "roster for %s has a self-contact", jid);
 	end
 	if not err then
-		hosts[host].events.fire_event("roster-load", username, host, roster);
+		hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster });
 	end
 	return roster, err;
 end
--- a/core/s2smanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/s2smanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -70,14 +70,14 @@
 function destroy_session(session, reason)
 	if session.destroyed then return; end
 	(session.log or log)("debug", "Destroying "..tostring(session.direction).." session "..tostring(session.from_host).."->"..tostring(session.to_host)..(reason and (": "..reason) or ""));
-	
+
 	if session.direction == "outgoing" then
 		hosts[session.from_host].s2sout[session.to_host] = nil;
 		session:bounce_sendq(reason);
 	elseif session.direction == "incoming" then
 		incoming_s2s[session] = nil;
 	end
-	
+
 	local event_data = { session = session, reason = reason };
 	if session.type == "s2sout" then
 		fire_event("s2sout-destroyed", event_data);
@@ -90,7 +90,7 @@
 			hosts[session.to_host].events.fire_event("s2sin-destroyed", event_data);
 		end
 	end
-	
+
 	retire_session(session, reason); -- Clean session until it is GC'd
 	return true;
 end
--- a/core/sessionmanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/sessionmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -10,8 +10,8 @@
 local pairs, next= pairs, next;
 
 local hosts = hosts;
-local full_sessions = full_sessions;
-local bare_sessions = bare_sessions;
+local full_sessions = prosody.full_sessions;
+local bare_sessions = prosody.bare_sessions;
 
 local logger = require "util.logger";
 local log = logger.init("sessionmanager");
@@ -44,7 +44,7 @@
 	session.ip = conn:ip();
 	local conn_name = "c2s"..tostring(session):match("[a-f0-9]+$");
 	session.log = logger.init(conn_name);
-		
+
 	return session;
 end
 
@@ -67,25 +67,26 @@
 
 	function session.send(data) log("debug", "Discarding data sent to resting session: %s", tostring(data)); return false; end
 	function session.data(data) log("debug", "Discarding data received from resting session: %s", tostring(data)); end
+	session.thread = { run = function (_, data) return session.data(data) end };
 	return setmetatable(session, resting_session);
 end
 
 function destroy_session(session, err)
 	(session.log or log)("debug", "Destroying session for %s (%s@%s)%s", session.full_jid or "(unknown)", session.username or "(unknown)", session.host or "(unknown)", err and (": "..err) or "");
 	if session.destroyed then return; end
-	
+
 	-- Remove session/resource from user's session list
 	if session.full_jid then
 		local host_session = hosts[session.host];
-		
+
 		-- Allow plugins to prevent session destruction
 		if host_session.events.fire_event("pre-resource-unbind", {session=session, error=err}) then
 			return;
 		end
-		
+
 		host_session.sessions[session.username].sessions[session.resource] = nil;
 		full_sessions[session.full_jid] = nil;
-		
+
 		if not next(host_session.sessions[session.username].sessions) then
 			log("debug", "All resources of %s are now offline", session.username);
 			host_session.sessions[session.username] = nil;
@@ -94,7 +95,7 @@
 
 		host_session.events.fire_event("resource-unbind", {session=session, error=err});
 	end
-	
+
 	retire_session(session);
 end
 
@@ -116,10 +117,20 @@
 	if session.resource then return nil, "cancel", "already-bound", "Cannot bind multiple resources on a single connection"; end
 	-- We don't support binding multiple resources
 
+	local event_payload = { session = session, resource = resource };
+	if hosts[session.host].events.fire_event("pre-resource-bind", event_payload) == false then
+		local err = event_payload.error;
+		if err then return nil, err.type, err.condition, err.text; end
+		return nil, "cancel", "not-allowed";
+	else
+		-- In case a plugin wants to poke at it
+		resource = event_payload.resource;
+	end
+
 	resource = resourceprep(resource);
 	resource = resource ~= "" and resource or uuid_generate();
 	--FIXME: Randomly-generated resources must be unique per-user, and never conflict with existing
-	
+
 	if not hosts[session.host].sessions[session.username] then
 		local sessions = { sessions = {} };
 		hosts[session.host].sessions[session.username] = sessions;
@@ -156,12 +167,12 @@
 			end
 		end
 	end
-	
+
 	session.resource = resource;
 	session.full_jid = session.username .. '@' .. session.host .. '/' .. resource;
 	hosts[session.host].sessions[session.username].sessions[resource] = session;
 	full_sessions[session.full_jid] = session;
-	
+
 	local err;
 	session.roster, err = rm_load_roster(session.username, session.host);
 	if err then
@@ -176,9 +187,9 @@
 		session.log("error", "Roster loading failed: %s", err);
 		return nil, "cancel", "internal-server-error", "Error loading roster";
 	end
-	
+
 	hosts[session.host].events.fire_event("resource-bind", {session=session});
-	
+
 	return true;
 end
 
--- a/core/stanza_router.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/stanza_router.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -46,7 +46,7 @@
 		if origin.send then
 			origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
 		end
-	elseif not((name == "features" or name == "error") and xmlns == "http://etherx.jabber.org/streams") then -- FIXME remove check once we handle S2S features
+	else
 		log("warn", "Unhandled %s stream element or stanza: %s; xmlns=%s: %s", origin_type, name, xmlns, tostring(stanza)); -- we didn't handle it
 		origin:close("unsupported-stanza-type");
 	end
@@ -199,7 +199,7 @@
 	-- Auto-detect origin if not specified
 	origin = origin or hosts[from_host];
 	if not origin then return false; end
-	
+
 	if hosts[host] then
 		-- old stanza routing code removed
 		core_post_stanza(origin, stanza);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/statsmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,68 @@
+
+local stats = require "util.statistics".new();
+local config = require "core.configmanager";
+local log = require "util.logger".init("stats");
+local timer = require "util.timer";
+local fire_event = prosody.events.fire_event;
+
+local stats_config = config.get("*", "statistics_interval");
+local stats_interval = tonumber(stats_config);
+if stats_config and not stats_interval then
+	log("error", "Invalid 'statistics_interval' setting, statistics will be disabled");
+end
+
+local measure, collect;
+local latest_stats = {};
+local changed_stats = {};
+local stats_extra = {};
+
+if stats_interval then
+	log("debug", "Statistics collection is enabled every %d seconds", stats_interval);
+	function measure(type, name)
+		local f = assert(stats[type], "unknown stat type: "..type);
+		return f(name);
+	end
+
+	local mark_collection_start = measure("times", "stats.collection");
+	local mark_processing_start = measure("times", "stats.processing");
+
+	function collect()
+		local mark_collection_done = mark_collection_start();
+		changed_stats, stats_extra = {}, {};
+		for stat_name, getter in pairs(stats.get_stats()) do
+			local type, value, extra = getter();
+			local old_value = latest_stats[stat_name];
+			latest_stats[stat_name] = value;
+			if value ~= old_value then
+				changed_stats[stat_name] = value;
+			end
+			if extra then
+				stats_extra[stat_name] = extra;
+			end
+		end
+		mark_collection_done();
+		local mark_processing_done = mark_processing_start();
+		fire_event("stats-updated", { stats = latest_stats, changed_stats = changed_stats, stats_extra = stats_extra });
+		mark_processing_done();
+		return stats_interval;
+	end
+
+	timer.add_task(stats_interval, collect);
+	prosody.events.add_handler("server-started", function () collect() end, -1);
+else
+	log("debug", "Statistics collection is disabled");
+	-- nop
+	function measure()
+		return measure;
+	end
+	function collect()
+	end
+end
+
+return {
+	measure = measure;
+	collect = collect;
+	get_stats = function ()
+		return latest_stats, changed_stats, stats_extra;
+	end;
+};
--- a/core/storagemanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/storagemanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,5 +1,5 @@
 
-local error, type, pairs = error, type, pairs;
+local type, pairs = type, pairs;
 local setmetatable = setmetatable;
 
 local config = require "core.configmanager";
@@ -37,7 +37,7 @@
 		local item = event.item;
 		stores_available:set(host, item.name, item);
 	end);
-	
+
 	host_session.events.add_handler("item-removed/storage-provider", function (event)
 		local item = event.item;
 		stores_available:set(host, item.name, nil);
@@ -70,7 +70,7 @@
 	if not driver_name then
 		driver_name = config.get(host, "default_storage") or "internal";
 	end
-	
+
 	local driver = load_driver(host, driver_name);
 	if not driver then
 		log("warn", "Falling back to null driver for %s storage on %s", store, host);
--- a/core/usermanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/core/usermanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -10,7 +10,6 @@
 local log = require "util.logger".init("usermanager");
 local type = type;
 local ipairs = ipairs;
-local pairs = pairs;
 local jid_bare = require "util.jid".bare;
 local jid_prep = require "util.jid".prep;
 local config = require "core.configmanager";
@@ -39,7 +38,7 @@
 function initialize_host(host)
 	local host_session = hosts[host];
 	if host_session.type ~= "local" then return; end
-	
+
 	host_session.events.add_handler("item-added/auth-provider", function (event)
 		local provider = event.item;
 		local auth_provider = config.get(host, "authentication") or default_provider;
@@ -115,10 +114,10 @@
 	local is_admin;
 	jid = jid_bare(jid);
 	host = host or "*";
-	
+
 	local host_admins = config.get(host, "admins");
 	local global_admins = config.get("*", "admins");
-	
+
 	if host_admins and host_admins ~= global_admins then
 		if type(host_admins) == "table" then
 			for _,admin in ipairs(host_admins) do
@@ -131,7 +130,7 @@
 			log("error", "Option 'admins' for host '%s' is not a list", host);
 		end
 	end
-	
+
 	if not is_admin and global_admins then
 		if type(global_admins) == "table" then
 			for _,admin in ipairs(global_admins) do
@@ -144,7 +143,7 @@
 			log("error", "Global option 'admins' is not a list");
 		end
 	end
-	
+
 	-- Still not an admin, check with auth provider
 	if not is_admin and host ~= "*" and hosts[host].users and hosts[host].users.is_admin then
 		is_admin = hosts[host].users.is_admin(jid);
--- a/fallbacks/bit.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/fallbacks/bit.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/fallbacks/lxp.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/fallbacks/lxp.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -61,7 +61,7 @@
 		while #data == 0 do data = coroutine.yield(); end
 		return data:sub(1,1);
 	end
-	
+
 	local ns = { xml = "http://www.w3.org/XML/1998/namespace" };
 	ns.__index = ns;
 	local function apply_ns(name, dodefault)
@@ -100,7 +100,7 @@
 		ns = getmetatable(ns);
 		return tag;
 	end
-	
+
 	while true do
 		if peek() == "<" then
 			local elem = read_until(">"):sub(2,-2);
--- a/net/adns.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/net/adns.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -65,7 +65,7 @@
 			if resolver.socketset[conn] == resolver.best_server and resolver.best_server == #servers then
 				log("error", "Exhausted all %d configured DNS servers, next lookup will try %s again", #servers, servers[1]);
 			end
-		
+
 			resolver:servfail(conn); -- Let the magic commence
 		end
 	end
@@ -73,7 +73,7 @@
 	if not handler then
 		return nil, err;
 	end
-	
+
 	handler.settimeout = function () end
 	handler.setsockname = function (_, ...) return sock:setsockname(...); end
 	handler.setpeername = function (_, ...) peername = (...); local ret, err = sock:setpeername(...); _:set_send(dummy_send); return ret, err; end
--- a/net/dns.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/net/dns.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -14,6 +14,7 @@
 
 local socket = require "socket";
 local timer = require "util.timer";
+local new_ip = require "util.ip".new_ip;
 
 local _, windows = pcall(require, "util.windows");
 local is_windows = (_ and windows) or os.getenv("WINDIR");
@@ -599,11 +600,12 @@
 		if resolv_conf then
 			for line in resolv_conf:lines() do
 				line = line:gsub("#.*$", "")
-					:match('^%s*nameserver%s+(.*)%s*$');
+					:match('^%s*nameserver%s+([%x:%.]*)%s*$');
 				if line then
-					line:gsub("%f[%d.](%d+%.%d+%.%d+%.%d+)%f[^%d.]", function (address)
-						self:addnameserver(address)
-					end);
+					local ip = new_ip(line);
+					if ip then
+						self:addnameserver(ip.addr);
+					end
 				end
 			end
 		end
@@ -623,7 +625,12 @@
 	if sock then return sock; end
 
 	local ok, err;
-	sock, err = socket.udp();
+	local peer = self.server[servernum];
+	if peer:find(":") then
+		sock, err = socket.udp6();
+	else
+		sock, err = socket.udp();
+	end
 	if sock and self.socket_wrapper then sock, err = self.socket_wrapper(sock, self); end
 	if not sock then
 		return nil, err;
@@ -636,7 +643,7 @@
 	-- if so, try the next server
 	ok, err = sock:setsockname('*', 0);
 	if not ok then return self:servfail(sock, err); end
-	ok, err = sock:setpeername(self.server[servernum], 53);
+	ok, err = sock:setpeername(peer, 53);
 	if not ok then return self:servfail(sock, err); end
 	return sock;
 end
@@ -761,7 +768,7 @@
 		return nil, err;
 	end
 	conn:send (o.packet)
-	
+
 	if timer and self.timeout then
 		local num_servers = #self.server;
 		local i = 1;
@@ -858,7 +865,7 @@
 					-- retire the query
 					local queries = self.active[response.header.id];
 					queries[response.question.raw] = nil;
-					
+
 					if not next(queries) then self.active[response.header.id] = nil; end
 					if not next(self.active) then self:closeall(); end
 
@@ -872,7 +879,7 @@
 						set(self.wanted, q.class, q.type, q.name, nil);
 					end
 				end
-				
+
 			end
 		end
 	end
--- a/net/http.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/net/http.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -37,7 +37,7 @@
 	if req.query then
 		t_insert(request_line, 4, "?"..req.query);
 	end
-	
+
 	conn:write(t_concat(request_line));
 	local t = { [2] = ": ", [4] = "\r\n" };
 	for k, v in pairs(req.headers) do
@@ -45,7 +45,7 @@
 		conn:write(t_concat(t));
 	end
 	conn:write("\r\n");
-	
+
 	if req.body then
 		conn:write(req.body);
 	end
@@ -85,12 +85,12 @@
 			end
 			destroy_request(request);
 		end
-		
+
 		if not data then
 			error_cb(err);
 			return;
 		end
-		
+
 		local function success_cb(r)
 			if request.callback then
 				request.callback(r.body, r.code, r, request);
@@ -109,18 +109,18 @@
 local function handleerr(err) log("error", "Traceback[http]: %s", traceback(tostring(err), 2)); end
 function request(u, ex, callback)
 	local req = url.parse(u);
-	
+
 	if not (req and req.host) then
 		callback(nil, 0, req);
 		return nil, "invalid-url";
 	end
-	
+
 	if not req.path then
 		req.path = "/";
 	end
-	
+
 	local method, headers, body;
-	
+
 	local host, port = req.host, req.port;
 	local host_header = host;
 	if (port == "80" and req.scheme == "http")
@@ -134,7 +134,7 @@
 		["Host"] = host_header;
 		["User-Agent"] = "Prosody XMPP Server";
 	};
-	
+
 	if req.userinfo then
 		headers["Authorization"] = "Basic "..b64(req.userinfo);
 	end
@@ -154,16 +154,16 @@
 			end
 		end
 	end
-	
+
 	-- Attach to request object
 	req.method, req.headers, req.body = method, headers, body;
-	
+
 	local using_https = req.scheme == "https";
 	if using_https and not ssl_available then
 		error("SSL not available, unable to contact https URL");
 	end
 	local port_number = port and tonumber(port) or (using_https and 443 or 80);
-	
+
 	-- Connect the socket, and wrap it with net.server
 	local conn = socket.tcp();
 	conn:settimeout(10);
@@ -172,7 +172,7 @@
 		callback(nil, 0, req);
 		return nil, err;
 	end
-	
+
 	local sslctx = false;
 	if using_https then
 		sslctx = ex and ex.sslctx or { mode = "client", protocol = "sslv23", options = { "no_sslv2", "no_sslv3" } };
@@ -180,7 +180,7 @@
 
 	req.handler, req.conn = assert(server.wrapclient(conn, host, port_number, listener, "*a", sslctx));
 	req.write = function (...) return req.handler:write(...); end
-	
+
 	req.callback = function (content, code, request, response) log("debug", "Calling callback, status %s", code or "---"); return select(2, xpcall(function () return callback(content, code, request, response) end, handleerr)); end
 	req.reader = request_reader;
 	req.state = "status";
--- a/net/http/server.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/net/http/server.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -189,6 +189,7 @@
 		persistent = persistent;
 		conn = conn;
 		send = _M.send_response;
+		done = _M.finish_response;
 		finish_cb = finish_cb;
 	};
 	conn._http_open_response = response;
@@ -208,7 +209,7 @@
 			err_code, err = 400, "Missing or invalid 'Host' header";
 		end
 	end
-	
+
 	if err then
 		response.status_code = err_code;
 		response:send(events.fire_event("http-error", { code = err_code, message = err }));
@@ -217,7 +218,7 @@
 
 	local event = request.method.." "..host..request.path:match("[^?]*");
 	local payload = { request = request, response = response };
-	--log("debug", "Firing event: %s", event);
+	log("debug", event);
 	local result = events.fire_event(event, payload);
 	if result ~= nil then
 		if result ~= true then
@@ -250,24 +251,30 @@
 	response.status_code = 404;
 	response:send(events.fire_event("http-error", { code = 404 }));
 end
-function _M.send_response(response, body)
-	if response.finished then return; end
-	response.finished = true;
-	response.conn._http_open_response = nil;
-	
+local function prepare_header(response)
 	local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]);
 	local headers = response.headers;
-	body = body or response.body or "";
-	headers.content_length = #body;
-
 	local output = { status_line };
 	for k,v in pairs(headers) do
 		t_insert(output, headerfix[k]..v);
 	end
 	t_insert(output, "\r\n\r\n");
+	return output;
+end
+_M.prepare_header = prepare_header;
+function _M.send_response(response, body)
+	if response.finished then return; end
+	body = body or response.body or "";
+	response.headers.content_length = #body;
+	local output = prepare_header(response);
 	t_insert(output, body);
-
 	response.conn:write(t_concat(output));
+	response:done();
+end
+function _M.finish_response(response)
+	if response.finished then return; end
+	response.finished = true;
+	response.conn._http_open_response = nil;
 	if response.on_destroy then
 		response:on_destroy();
 		response.on_destroy = nil;
--- a/net/server.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/net/server.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/net/server_event.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/net/server_event.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -115,10 +115,10 @@
 local interface_mt
 do
 	interface_mt = {}; interface_mt.__index = interface_mt;
-	
+
 	local addevent = base.addevent
 	local coroutine_wrap, coroutine_yield = coroutine.wrap,coroutine.yield
-	
+
 	-- Private methods
 	function interface_mt:_position(new_position)
 			self.position = new_position or self.position
@@ -127,7 +127,7 @@
 	function interface_mt:_close()
 		return self:_destroy();
 	end
-	
+
 	function interface_mt:_start_connection(plainssl) -- should be called from addclient
 			local callback = function( event )
 				if EV_TIMEOUT == event then  -- timeout during connection
@@ -268,12 +268,12 @@
 			interfacelist( "delete", self )
 			return true
 	end
-	
+
 	function interface_mt:_lock(nointerface, noreading, nowriting)  -- lock or unlock this interface or events
 			self.nointerface, self.noreading, self.nowriting = nointerface, noreading, nowriting
 			return nointerface, noreading, nowriting
 	end
-	
+
 	--TODO: Deprecate
 	function interface_mt:lock_read(switch)
 		if switch then
@@ -300,7 +300,7 @@
 		end
 		return self._connections
 	end
-	
+
 	-- Public methods
 	function interface_mt:write(data)
 		if self.nowriting then return nil, "locked" end
@@ -343,27 +343,27 @@
 			return true
 		end
 	end
-	
+
 	function interface_mt:socket()
 		return self.conn
 	end
-	
+
 	function interface_mt:server()
 		return self._server or self;
 	end
-	
+
 	function interface_mt:port()
 		return self._port
 	end
-	
+
 	function interface_mt:serverport()
 		return self._serverport
 	end
-	
+
 	function interface_mt:ip()
 		return self._ip
 	end
-	
+
 	function interface_mt:ssl()
 		return self._usingssl
 	end
@@ -372,15 +372,15 @@
 	function interface_mt:type()
 		return self._type or "client"
 	end
-	
+
 	function interface_mt:connections()
 		return self._connections
 	end
-	
+
 	function interface_mt:address()
 		return self.addr
 	end
-	
+
 	function interface_mt:set_sslctx(sslctx)
 		self._sslctx = sslctx;
 		if sslctx then
@@ -396,11 +396,11 @@
 		end
 		return self._pattern;
 	end
-	
+
 	function interface_mt:set_send(new_send)
 		-- No-op, we always use the underlying connection's send
 	end
-	
+
 	function interface_mt:starttls(sslctx, call_onconnect)
 		debug( "try to start ssl at client id:", self.id )
 		local err
@@ -429,22 +429,22 @@
 		self.starttls = false;
 		return true
 	end
-	
+
 	function interface_mt:setoption(option, value)
 		if self.conn.setoption then
 			return self.conn:setoption(option, value);
 		end
 		return false, "setoption not implemented";
 	end
-	
+
 	function interface_mt:setlistener(listener)
 		self:ondetach(); -- Notify listener that it is no longer responsible for this connection
-		self.onconnect, self.ondisconnect, self.onincoming,
-		self.ontimeout, self.onstatus, self.ondetach
-			= listener.onconnect, listener.ondisconnect, listener.onincoming,
-			listener.ontimeout, listener.onstatus, listener.ondetach;
+		self.onconnect, self.ondisconnect, self.onincoming, self.ontimeout,
+		self.onreadtimeout, self.onstatus, self.ondetach
+			= listener.onconnect, listener.ondisconnect, listener.onincoming, listener.ontimeout,
+			  listener.onreadtimeout, listener.onstatus, listener.ondetach;
 	end
-	
+
 	-- Stub handlers
 	function interface_mt:onconnect()
 	end
@@ -454,6 +454,12 @@
 	end
 	function interface_mt:ontimeout()
 	end
+	function interface_mt:onreadtimeout()
+		self.fatalerror = "timeout during receiving"
+		debug( "connection failed:", self.fatalerror )
+		self:_close()
+		self.eventread = nil
+	end
 	function interface_mt:ondrain()
 	end
 	function interface_mt:ondetach()
@@ -483,6 +489,7 @@
 			ondisconnect = listener.ondisconnect;  -- will be called when client disconnects
 			onincoming = listener.onincoming;  -- will be called when client sends data
 			ontimeout = listener.ontimeout; -- called when fatal socket timeout occurs
+			onreadtimeout = listener.onreadtimeout; -- called when socket inactivity timeout occurs
 			ondrain = listener.ondrain; -- called when writebuffer is empty
 			ondetach = listener.ondetach; -- called when disassociating this listener from this connection
 			onstatus = listener.onstatus; -- called for status changes (e.g. of SSL/TLS)
@@ -498,7 +505,7 @@
 			noreading = false, nowriting = false;  -- locks of the read/writecallback
 			startsslcallback = false;  -- starting handshake callback
 			position = false;  -- position of client in interfacelist
-			
+
 			-- Properties
 			_ip = ip, _port = port, _server = server, _pattern = pattern,
 			_serverport = (server and server:port() or nil),
@@ -574,7 +581,7 @@
 				end
 			end
 		end
-		
+
 		interface.readcallback = function( event )  -- called on read events
 			--vdebug( "new client read event, id/ip/port:", tostring(interface.id), tostring(ip), tostring(port) )
 			if interface.noreading or interface.fatalerror then  -- leave this event
@@ -582,61 +589,56 @@
 				interface.eventread = nil
 				return -1
 			end
-			if EV_TIMEOUT == event then  -- took too long to get some data from client -> disconnect
-				interface.fatalerror = "timeout during receiving"
-				debug( "connection failed:", interface.fatalerror )
+			if EV_TIMEOUT == event and interface:onreadtimeout() ~= true then
+				return -1 -- took too long to get some data from client -> disconnect
+			end
+			if interface._usingssl then  -- handle luasec
+				if interface.eventwritetimeout then  -- ok, in the past writecallback was regged
+					local ret = interface.writecallback( )  -- call it
+					--vdebug( "tried to write in readcallback, result:", tostring(ret) )
+				end
+				if interface.eventreadtimeout then
+					interface.eventreadtimeout:close( )
+					interface.eventreadtimeout = nil
+				end
+			end
+			local buffer, err, part = interface.conn:receive( interface._pattern )  -- receive buffer with "pattern"
+			--vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) )
+			buffer = buffer or part
+			if buffer and #buffer > cfg.MAX_READ_LENGTH then  -- check buffer length
+				interface.fatalerror = "receive buffer exceeded"
+				debug( "fatal error:", interface.fatalerror )
 				interface:_close()
 				interface.eventread = nil
 				return -1
-			else -- can read
-				if interface._usingssl then  -- handle luasec
-					if interface.eventwritetimeout then  -- ok, in the past writecallback was regged
-						local ret = interface.writecallback( )  -- call it
-						--vdebug( "tried to write in readcallback, result:", tostring(ret) )
+			end
+			if err and ( err ~= "timeout" and err ~= "wantread" ) then
+				if "wantwrite" == err then -- need to read on write event
+					if not interface.eventwrite then  -- register new write event if needed
+						interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT )
 					end
-					if interface.eventreadtimeout then
-						interface.eventreadtimeout:close( )
-						interface.eventreadtimeout = nil
-					end
-				end
-				local buffer, err, part = interface.conn:receive( interface._pattern )  -- receive buffer with "pattern"
-				--vdebug( "read data:", tostring(buffer), "error:", tostring(err), "part:", tostring(part) )
-				buffer = buffer or part
-				if buffer and #buffer > cfg.MAX_READ_LENGTH then  -- check buffer length
-					interface.fatalerror = "receive buffer exceeded"
-					debug( "fatal error:", interface.fatalerror )
+					interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT,
+						function( )
+							interface:_close()
+						end, cfg.READ_TIMEOUT
+					)
+					debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." )
+					-- to be honest i dont know what happens next, if it is allowed to first read, the write etc...
+				else  -- connection was closed or fatal error
+					interface.fatalerror = err
+					debug( "connection failed in read event:", interface.fatalerror )
 					interface:_close()
 					interface.eventread = nil
 					return -1
 				end
-				if err and ( err ~= "timeout" and err ~= "wantread" ) then
-					if "wantwrite" == err then -- need to read on write event
-						if not interface.eventwrite then  -- register new write event if needed
-							interface.eventwrite = addevent( base, interface.conn, EV_WRITE, interface.writecallback, cfg.WRITE_TIMEOUT )
-						end
-						interface.eventreadtimeout = addevent( base, nil, EV_TIMEOUT,
-							function( )
-								interface:_close()
-							end, cfg.READ_TIMEOUT
-						)
-						debug( "wantwrite during read attempt, reg it in writecallback but dont know what really happens next..." )
-						-- to be honest i dont know what happens next, if it is allowed to first read, the write etc...
-					else  -- connection was closed or fatal error
-						interface.fatalerror = err
-						debug( "connection failed in read event:", interface.fatalerror )
-						interface:_close()
-						interface.eventread = nil
-						return -1
-					end
-				else
-					interface.onincoming( interface, buffer, err )  -- send new data to listener
-				end
-				if interface.noreading then
-					interface.eventread = nil;
-					return -1;
-				end
-				return EV_READ, cfg.READ_TIMEOUT
+			else
+				interface.onincoming( interface, buffer, err )  -- send new data to listener
 			end
+			if interface.noreading then
+				interface.eventread = nil;
+				return -1;
+			end
+			return EV_READ, cfg.READ_TIMEOUT
 		end
 
 		client:settimeout( 0 )  -- set non blocking
@@ -652,7 +654,7 @@
 		debug "creating server interface..."
 		local interface = {
 			_connections = 0;
-			
+
 			conn = server;
 			onconnect = listener.onconnect;  -- will be called when new client connected
 			eventread = false;  -- read event handler
@@ -660,7 +662,7 @@
 			readcallback = false; -- read event callback
 			fatalerror = false; -- error message
 			nointerface = true;  -- lock/unlock parameter
-			
+
 			_ip = addr, _port = port, _pattern = pattern,
 			_sslctx = sslctx;
 		}
@@ -699,12 +701,12 @@
 					clientinterface:_start_session( true )
 				end
 				debug( "accepted incoming client connection from:", client_ip or "<unknown IP>", client_port or "<unknown port>", "to", port or "<unknown port>");
-				
+
 				client, err = server:accept()    -- try to accept again
 			end
 			return EV_READ
 		end
-		
+
 		server:settimeout( 0 )
 		setmetatable(interface, interface_mt)
 		interfacelist( "add", interface )
@@ -747,7 +749,7 @@
 		return interface, client
 		--function handleclient( client, ip, port, server, pattern, listener, _, sslctx )  -- creates an client interface
 	end
-	
+
 	function addclient( addr, serverport, listener, pattern, localaddr, localport, sslcfg, startssl )
 		local client, err = socket.tcp()  -- creating new socket
 		if not client then
@@ -838,14 +840,14 @@
 
 local function link(sender, receiver, buffersize)
 	local sender_locked;
-	
+
 	function receiver:ondrain()
 		if sender_locked then
 			sender:resume();
 			sender_locked = nil;
 		end
 	end
-	
+
 	function sender:onincoming(data)
 		receiver:write(data);
 		if receiver.writebufferlen >= buffersize then
--- a/net/server_select.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/net/server_select.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
--- 
+--
 -- server.lua by blastbeat of the luadch project
 -- Re-used here under the MIT/X Consortium License
--- 
+--
 -- Modifications (C) 2008-2010 Matthew Wild, Waqas Hussain
 --
 
@@ -145,7 +145,7 @@
 _maxsendlen = 51000 * 1024 -- max len of send buffer
 _maxreadlen = 25000 * 1024 -- max len of read buffer
 
-_checkinterval = 1200000 -- interval in secs to check idle clients
+_checkinterval = 30 -- interval in secs to check idle clients
 _sendtimeout = 60000 -- allowed send idle time in secs
 _readtimeout = 6 * 60 * 60 -- allowed read idle time in secs
 
@@ -284,6 +284,7 @@
 	local status = listeners.onstatus
 	local disconnect = listeners.ondisconnect
 	local drain = listeners.ondrain
+	local onreadtimeout = listeners.onreadtimeout;
 	local detach = listeners.ondetach
 
 	local bufferqueue = { } -- buffer array
@@ -313,6 +314,8 @@
 	handler.disconnect = function( )
 		return disconnect
 	end
+	handler.onreadtimeout = onreadtimeout;
+
 	handler.setlistener = function( self, listeners )
 		if detach then
 			detach(self) -- Notify listener that it is no longer responsible for this connection
@@ -321,6 +324,7 @@
 		disconnect = listeners.ondisconnect
 		status = listeners.onstatus
 		drain = listeners.ondrain
+		handler.onreadtimeout = listeners.onreadtimeout
 		detach = listeners.ondetach
 	end
 	handler.getstats = function( )
@@ -564,6 +568,9 @@
 						_ = status and status( handler, "ssl-handshake-complete" )
 						if self.autostart_ssl and listeners.onconnect then
 							listeners.onconnect(self);
+							if bufferqueuelen ~= 0 then
+								_sendlistlen = addsocket(_sendlist, client, _sendlistlen)
+							end
 						end
 						_readlistlen = addsocket(_readlist, client, _readlistlen)
 						return true
@@ -613,7 +620,7 @@
 			shutdown = id
 			_socketlist[ socket ] = handler
 			_readlistlen = addsocket(_readlist, socket, _readlistlen)
-			
+
 			-- remove traces of the old socket
 			_readlistlen = removesocket( _readlist, oldsocket, _readlistlen )
 			_sendlistlen = removesocket( _sendlist, oldsocket, _sendlistlen )
@@ -701,7 +708,7 @@
 			sender_locked = nil;
 		end
 	end
-	
+
 	local _readbuffer = sender.readbuffer;
 	function sender.readbuffer()
 		_readbuffer();
@@ -870,16 +877,18 @@
 			_starttime = _currenttime
 			for handler, timestamp in pairs( _writetimes ) do
 				if os_difftime( _currenttime - timestamp ) > _sendtimeout then
-					--_writetimes[ handler ] = nil
 					handler.disconnect( )( handler, "send timeout" )
 					handler:force_close()	 -- forced disconnect
 				end
 			end
 			for handler, timestamp in pairs( _readtimes ) do
 				if os_difftime( _currenttime - timestamp ) > _readtimeout then
-					--_readtimes[ handler ] = nil
-					handler.disconnect( )( handler, "read timeout" )
-					handler:close( )	-- forced disconnect?
+					if not(handler.onreadtimeout) or handler:onreadtimeout() ~= true then
+						handler.disconnect( )( handler, "read timeout" )
+						handler:close( )	-- forced disconnect?
+					else
+						_readtimes[ handler ] = _currenttime -- reset timer
+					end
 				end
 			end
 		end
@@ -940,9 +949,9 @@
 	client:settimeout( 0 )
 	_, err = client:connect( address, port )
 	if err then -- try again
-		local handler = wrapclient( client, address, port, listeners )
+		return wrapclient( client, address, port, listeners, pattern, sslctx )
 	else
-		wrapconnection( nil, listeners, client, address, port, "clientport", pattern, sslctx )
+		return wrapconnection( nil, listeners, client, address, port, "clientport", pattern, sslctx )
 	end
 end
 
@@ -972,7 +981,7 @@
 
 	addclient = addclient,
 	wrapclient = wrapclient,
-	
+
 	loop = loop,
 	link = link,
 	step = step,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/websocket.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,272 @@
+-- Prosody IM
+-- Copyright (C) 2012 Florian Zeitz
+-- Copyright (C) 2014 Daurnimator
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local t_concat = table.concat;
+
+local http = require "net.http";
+local frames = require "net.websocket.frames";
+local base64 = require "util.encodings".base64;
+local sha1 = require "util.hashes".sha1;
+local random_bytes = require "util.random".bytes;
+local timer = require "util.timer";
+local log = require "util.logger".init "websocket";
+
+local close_timeout = 3; -- Seconds to wait after sending close frame until closing connection.
+
+local websockets = {};
+
+local websocket_listeners = {};
+function websocket_listeners.ondisconnect(handler, err)
+	local s = websockets[handler];
+	websockets[handler] = nil;
+	if s.close_timer then
+		timer.stop(s.close_timer);
+		s.close_timer = nil;
+	end
+	s.readyState = 3;
+	if s.close_code == nil and s.onerror then s:onerror(err); end
+	if s.onclose then s:onclose(s.close_code, s.close_message or err); end
+end
+
+function websocket_listeners.ondetach(handler)
+	websockets[handler] = nil;
+end
+
+local function fail(s, code, reason)
+	module:log("warn", "WebSocket connection failed, closing. %d %s", code, reason);
+	s:close(code, reason);
+	s.handler:close();
+	return false
+end
+
+function websocket_listeners.onincoming(handler, buffer, err)
+	local s = websockets[handler];
+	s.readbuffer = s.readbuffer..buffer;
+	while true do
+		local frame, len = frames.parse(s.readbuffer);
+		if frame == nil then break end
+		s.readbuffer = s.readbuffer:sub(len+1);
+
+		log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
+
+		-- Error cases
+		if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
+			return fail(s, 1002, "Reserved bits not zero");
+		end
+
+		if frame.opcode < 0x8 then
+			local databuffer = s.databuffer;
+			if frame.opcode == 0x0 then -- Continuation frames
+				if not databuffer then
+					return fail(s, 1002, "Unexpected continuation frame");
+				end
+				databuffer[#databuffer+1] = frame.data;
+			elseif frame.opcode == 0x1 or frame.opcode == 0x2 then -- Text or Binary frame
+				if databuffer then
+					return fail(s, 1002, "Continuation frame expected");
+				end
+				databuffer = {type=frame.opcode, frame.data};
+				s.databuffer = databuffer;
+			else
+				return fail(s, 1002, "Reserved opcode");
+			end
+			if frame.FIN then
+				s.databuffer = nil;
+				if s.onmessage then
+					s:onmessage(t_concat(databuffer), databuffer.type);
+				end
+			end
+		else -- Control frame
+			if frame.length > 125 then -- Control frame with too much payload
+				return fail(s, 1002, "Payload too large");
+			elseif not frame.FIN then -- Fragmented control frame
+				return fail(s, 1002, "Fragmented control frame");
+			end
+			if frame.opcode == 0x8 then -- Close request
+				if frame.length == 1 then
+					return fail(s, 1002, "Close frame with payload, but too short for status code");
+				end
+				local status_code, message = frames.parse_close(frame.data);
+				if status_code == nil then
+					--[[ RFC 6455 7.4.1
+					1005 is a reserved value and MUST NOT be set as a status code in a
+					Close control frame by an endpoint.  It is designated for use in
+					applications expecting a status code to indicate that no status
+					code was actually present.
+					]]
+					status_code = 1005
+				elseif status_code < 1000 then
+					return fail(s, 1002, "Closed with invalid status code");
+				elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
+					return fail(s, 1002, "Closed with reserved status code");
+				end
+				s.close_code, s.close_message = status_code, message;
+				s:close(1000);
+				return true;
+			elseif frame.opcode == 0x9 then -- Ping frame
+				frame.opcode = 0xA;
+				frame.MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked
+				handler:write(frames.build(frame));
+			elseif frame.opcode == 0xA then -- Pong frame
+				log("debug", "Received unexpected pong frame: " .. tostring(frame.data));
+			else
+				return fail(s, 1002, "Reserved opcode");
+			end
+		end
+	end
+	return true;
+end
+
+local websocket_methods = {};
+local function close_timeout_cb(now, timerid, s)
+	s.close_timer = nil;
+	log("warn", "Close timeout waiting for server to close, closing manually.");
+	s.handler:close();
+end
+function websocket_methods:close(code, reason)
+	if self.readyState < 2 then
+		code = code or 1000;
+		log("debug", "closing WebSocket with code %i: %s" , code , tostring(reason));
+		self.readyState = 2;
+		local handler = self.handler;
+		handler:write(frames.build_close(code, reason, true));
+		-- Do not close socket straight away, wait for acknowledgement from server.
+		self.close_timer = timer.add_task(close_timeout, close_timeout_cb, self);
+	elseif self.readyState == 2 then
+		log("debug", "tried to close a closing WebSocket, closing the raw socket.");
+		-- Stop timer
+		if self.close_timer then
+			timer.stop(self.close_timer);
+			self.close_timer = nil;
+		end
+		local handler = self.handler;
+		handler:close();
+	else
+		log("debug", "tried to close a closed WebSocket, ignoring.");
+	end
+end
+function websocket_methods:send(data, opcode)
+	if self.readyState < 1 then
+		return nil, "WebSocket not open yet, unable to send data.";
+	elseif self.readyState >= 2 then
+		return nil, "WebSocket closed, unable to send data.";
+	end
+	if opcode == "text" or opcode == nil then
+		opcode = 0x1;
+	elseif opcode == "binary" then
+		opcode = 0x2;
+	end
+	local frame = {
+		FIN = true;
+		MASK = true; -- RFC 6455 6.1.5: If the data is being sent by the client, the frame(s) MUST be masked
+		opcode = opcode;
+		data = tostring(data);
+	};
+	log("debug", "WebSocket sending frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
+	return self.handler:write(frames.build(frame));
+end
+
+local websocket_metatable = {
+	__index = websocket_methods;
+};
+
+local function connect(url, ex, listeners)
+	ex = ex or {};
+
+	--[[RFC 6455 4.1.7:
+		The request MUST include a header field with the name
+	|Sec-WebSocket-Key|.  The value of this header field MUST be a
+	nonce consisting of a randomly selected 16-byte value that has
+	been base64-encoded (see Section 4 of [RFC4648]).  The nonce
+	MUST be selected randomly for each connection.
+	]]
+	local key = base64.encode(random_bytes(16));
+
+	-- Either a single protocol string or an array of protocol strings.
+	local protocol = ex.protocol;
+	if type(protocol) == "string" then
+		protocol = { protocol, [protocol] = true };
+	elseif type(protocol) == "table" and protocol[1] then
+		for _, v in ipairs(protocol) do
+			protocol[v] = true;
+		end
+	else
+		protocol = nil;
+	end
+
+	local headers = {
+		["Upgrade"] = "websocket";
+		["Connection"] = "Upgrade";
+		["Sec-WebSocket-Key"] = key;
+		["Sec-WebSocket-Protocol"] = protocol and t_concat(protocol, ", ");
+		["Sec-WebSocket-Version"] = "13";
+		["Sec-WebSocket-Extensions"] = ex.extensions;
+	}
+	if ex.headers then
+		for k,v in pairs(ex.headers) do
+			headers[k] = v;
+		end
+	end
+
+	local s = setmetatable({
+		readbuffer = "";
+		databuffer = nil;
+		handler = nil;
+		close_code = nil;
+		close_message = nil;
+		close_timer = nil;
+		readyState = 0;
+		protocol = nil;
+
+		url = url;
+
+		onopen = listeners.onopen;
+		onclose = listeners.onclose;
+		onmessage = listeners.onmessage;
+		onerror = listeners.onerror;
+	}, websocket_metatable);
+
+	local http_url = url:gsub("^(ws)", "http");
+	local http_req = http.request(http_url, {
+		method = "GET";
+		headers = headers;
+		sslctx = ex.sslctx;
+	}, function(b, c, r, http_req)
+		if c ~= 101
+		   or r.headers["connection"]:lower() ~= "upgrade"
+		   or r.headers["upgrade"] ~= "websocket"
+		   or r.headers["sec-websocket-accept"] ~= base64.encode(sha1(key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
+		   or (protocol and not protocol[r.headers["sec-websocket-protocol"]])
+		   then
+			s.readyState = 3;
+			log("warn", "WebSocket connection to %s failed: %s", url, tostring(b));
+			if s.onerror then s:onerror("connecting-failed"); end
+			return;
+		end
+
+		s.protocol = r.headers["sec-websocket-protocol"];
+
+		-- Take possession of socket from http
+		http_req.conn = nil;
+		local handler = http_req.handler;
+		s.handler = handler;
+		websockets[handler] = s;
+		handler:setlistener(websocket_listeners);
+
+		log("debug", "WebSocket connected successfully to %s", url);
+		s.readyState = 1;
+		if s.onopen then s:onopen(); end
+		websocket_listeners.onincoming(handler, b);
+	end);
+
+	return s;
+end
+
+return {
+	connect = connect;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/net/websocket/frames.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,195 @@
+-- Prosody IM
+-- Copyright (C) 2012 Florian Zeitz
+-- Copyright (C) 2014 Daurnimator
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local softreq = require "util.dependencies".softreq;
+local log = require "util.logger".init "websocket.frames";
+local random_bytes = require "util.random".bytes;
+
+local bit;
+pcall(function() bit = require"bit"; end);
+bit = bit or softreq"bit32"
+if not bit then log("error", "No bit module found. Either LuaJIT 2, lua-bitop or Lua 5.2 is required"); end
+local band = bit.band;
+local bor = bit.bor;
+local bxor = bit.bxor;
+local lshift = bit.lshift;
+local rshift = bit.rshift;
+
+local t_concat = table.concat;
+local s_byte = string.byte;
+local s_char= string.char;
+local s_sub = string.sub;
+
+local function read_uint16be(str, pos)
+	local l1, l2 = s_byte(str, pos, pos+1);
+	return l1*256 + l2;
+end
+-- FIXME: this may lose precision
+local function read_uint64be(str, pos)
+	local l1, l2, l3, l4, l5, l6, l7, l8 = s_byte(str, pos, pos+7);
+	return lshift(l1, 56) + lshift(l2, 48) + lshift(l3, 40) + lshift(l4, 32)
+		+ lshift(l5, 24) + lshift(l6, 16) + lshift(l7, 8) + l8;
+end
+local function pack_uint16be(x)
+	return s_char(rshift(x, 8), band(x, 0xFF));
+end
+local function get_byte(x, n)
+	return band(rshift(x, n), 0xFF);
+end
+local function pack_uint64be(x)
+	return s_char(rshift(x, 56), get_byte(x, 48), get_byte(x, 40), get_byte(x, 32),
+		get_byte(x, 24), get_byte(x, 16), get_byte(x, 8), band(x, 0xFF));
+end
+
+local function parse_frame_header(frame)
+	if #frame < 2 then return; end
+
+	local byte1, byte2 = s_byte(frame, 1, 2);
+	local result = {
+		FIN = band(byte1, 0x80) > 0;
+		RSV1 = band(byte1, 0x40) > 0;
+		RSV2 = band(byte1, 0x20) > 0;
+		RSV3 = band(byte1, 0x10) > 0;
+		opcode = band(byte1, 0x0F);
+
+		MASK = band(byte2, 0x80) > 0;
+		length = band(byte2, 0x7F);
+	};
+
+	local length_bytes = 0;
+	if result.length == 126 then
+		length_bytes = 2;
+	elseif result.length == 127 then
+		length_bytes = 8;
+	end
+
+	local header_length = 2 + length_bytes + (result.MASK and 4 or 0);
+	if #frame < header_length then return; end
+
+	if length_bytes == 2 then
+		result.length = read_uint16be(frame, 3);
+	elseif length_bytes == 8 then
+		result.length = read_uint64be(frame, 3);
+	end
+
+	if result.MASK then
+		result.key = { s_byte(frame, length_bytes+3, length_bytes+6) };
+	end
+
+	return result, header_length;
+end
+
+-- XORs the string `str` with the array of bytes `key`
+-- TODO: optimize
+local function apply_mask(str, key, from, to)
+	from = from or 1
+	if from < 0 then from = #str + from + 1 end -- negative indicies
+	to = to or #str
+	if to < 0 then to = #str + to + 1 end -- negative indicies
+	local key_len = #key
+	local counter = 0;
+	local data = {};
+	for i = from, to do
+		local key_index = counter%key_len + 1;
+		counter = counter + 1;
+		data[counter] = s_char(bxor(key[key_index], s_byte(str, i)));
+	end
+	return t_concat(data);
+end
+
+local function parse_frame_body(frame, header, pos)
+	if header.MASK then
+		return apply_mask(frame, header.key, pos, pos + header.length - 1);
+	else
+		return frame:sub(pos, pos + header.length - 1);
+	end
+end
+
+local function parse_frame(frame)
+	local result, pos = parse_frame_header(frame);
+	if result == nil or #frame < (pos + result.length) then return; end
+	result.data = parse_frame_body(frame, result, pos+1);
+	return result, pos + result.length;
+end
+
+local function build_frame(desc)
+	local data = desc.data or "";
+
+	assert(desc.opcode and desc.opcode >= 0 and desc.opcode <= 0xF, "Invalid WebSocket opcode");
+	if desc.opcode >= 0x8 then
+		-- RFC 6455 5.5
+		assert(#data <= 125, "WebSocket control frames MUST have a payload length of 125 bytes or less.");
+	end
+
+	local b1 = bor(desc.opcode,
+		desc.FIN and 0x80 or 0,
+		desc.RSV1 and 0x40 or 0,
+		desc.RSV2 and 0x20 or 0,
+		desc.RSV3 and 0x10 or 0);
+
+	local b2 = #data;
+	local length_extra;
+	if b2 <= 125 then -- 7-bit length
+		length_extra = "";
+	elseif b2 <= 0xFFFF then -- 2-byte length
+		b2 = 126;
+		length_extra = pack_uint16be(#data);
+	else -- 8-byte length
+		b2 = 127;
+		length_extra = pack_uint64be(#data);
+	end
+
+	local key = ""
+	if desc.MASK then
+		local key_a = desc.key
+		if key_a then
+			key = s_char(unpack(key_a, 1, 4));
+		else
+			key = random_bytes(4);
+			key_a = {key:byte(1,4)};
+		end
+		b2 = bor(b2, 0x80);
+		data = apply_mask(data, key_a);
+	end
+
+	return s_char(b1, b2) .. length_extra .. key .. data
+end
+
+local function parse_close(data)
+	local code, message
+	if #data >= 2 then
+		code = read_uint16be(data, 1);
+		if #data > 2 then
+			message = s_sub(data, 3);
+		end
+	end
+	return code, message
+end
+
+local function build_close(code, message, mask)
+	local data = pack_uint16be(code);
+	if message then
+		assert(#message<=123, "Close reason must be <=123 bytes");
+		data = data .. message;
+	end
+	return build_frame({
+		opcode = 0x8;
+		FIN = true;
+		MASK = mask;
+		data = data;
+	});
+end
+
+return {
+	parse_header = parse_frame_header;
+	parse_body = parse_frame_body;
+	parse = parse_frame;
+	build = build_frame;
+	parse_close = parse_close;
+	build_close = build_close;
+};
--- a/plugins/adhoc/adhoc.lib.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/adhoc/adhoc.lib.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -25,12 +25,13 @@
 end
 
 function _M.handle_cmd(command, origin, stanza)
-	local sessionid = stanza.tags[1].attr.sessionid or uuid.generate();
+	local cmdtag = stanza.tags[1]
+	local sessionid = cmdtag.attr.sessionid or uuid.generate();
 	local dataIn = {};
 	dataIn.to = stanza.attr.to;
 	dataIn.from = stanza.attr.from;
-	dataIn.action = stanza.tags[1].attr.action or "execute";
-	dataIn.form = stanza.tags[1]:child_with_ns("jabber:x:data");
+	dataIn.action = cmdtag.attr.action or "execute";
+	dataIn.form = cmdtag:get_child("x", "jabber:x:data");
 
 	local data, state = command:handler(dataIn, states[sessionid]);
 	states[sessionid] = state;
--- a/plugins/adhoc/mod_adhoc.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/adhoc/mod_adhoc.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -6,79 +6,81 @@
 --
 
 local st = require "util.stanza";
+local keys = require "util.iterators".keys;
+local array_collect = require "util.array".collect;
 local is_admin = require "core.usermanager".is_admin;
+local jid_split = require "util.jid".split;
 local adhoc_handle_cmd = module:require "adhoc".handle_cmd;
 local xmlns_cmd = "http://jabber.org/protocol/commands";
-local xmlns_disco = "http://jabber.org/protocol/disco";
 local commands = {};
 
 module:add_feature(xmlns_cmd);
 
-module:hook("iq/host/"..xmlns_disco.."#info:query", function (event)
-	local origin, stanza = event.origin, event.stanza;
-	local node = stanza.tags[1].attr.node;
-	if stanza.attr.type == "get" and node then
-		if commands[node] then
-			local privileged = is_admin(stanza.attr.from, stanza.attr.to);
-			if (commands[node].permission == "admin" and privileged)
-			    or (commands[node].permission == "user") then
-				reply = st.reply(stanza);
-				reply:tag("query", { xmlns = xmlns_disco.."#info",
-				    node = node });
-				reply:tag("identity", { name = commands[node].name,
-				    category = "automation", type = "command-node" }):up();
-				reply:tag("feature", { var = xmlns_cmd }):up();
-				reply:tag("feature", { var = "jabber:x:data" }):up();
-			else
-				reply = st.error_reply(stanza, "auth", "forbidden", "This item is not available to you");
-			end
-			origin.send(reply);
-			return true;
-		elseif node == xmlns_cmd then
-			reply = st.reply(stanza);
-			reply:tag("query", { xmlns = xmlns_disco.."#info",
-			    node = node });
-			reply:tag("identity", { name = "Ad-Hoc Commands",
-			    category = "automation", type = "command-list" }):up();
-			origin.send(reply);
-			return true;
-
+module:hook("host-disco-info-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	if commands[node] then
+		local from = stanza.attr.from;
+		local privileged = is_admin(from, stanza.attr.to);
+		local global_admin = is_admin(from);
+		local username, hostname = jid_split(from);
+		local command = commands[node];
+		if (command.permission == "admin" and privileged)
+		    or (command.permission == "global_admin" and global_admin)
+		    or (command.permission == "local_user" and hostname == module.host)
+		    or (command.permission == "user") then
+			reply:tag("identity", { name = command.name,
+			    category = "automation", type = "command-node" }):up();
+			reply:tag("feature", { var = xmlns_cmd }):up();
+			reply:tag("feature", { var = "jabber:x:data" }):up();
+			event.exists = true;
+		else
+			return origin.send(st.error_reply(stanza, "auth", "forbidden", "This item is not available to you"));
 		end
+	elseif node == xmlns_cmd then
+		reply:tag("identity", { name = "Ad-Hoc Commands",
+		    category = "automation", type = "command-list" }):up();
+		    event.exists = true;
 	end
 end);
 
-module:hook("iq/host/"..xmlns_disco.."#items:query", function (event)
-	local origin, stanza = event.origin, event.stanza;
-	if stanza.attr.type == "get" and stanza.tags[1].attr.node
-	    and stanza.tags[1].attr.node == xmlns_cmd then
-		local admin = is_admin(stanza.attr.from, stanza.attr.to);
-		local global_admin = is_admin(stanza.attr.from);
-		reply = st.reply(stanza);
-		reply:tag("query", { xmlns = xmlns_disco.."#items",
-		    node = xmlns_cmd });
-		for node, command in pairs(commands) do
-			if (command.permission == "admin" and admin)
-			    or (command.permission == "global_admin" and global_admin)
-			    or (command.permission == "user") then
-				reply:tag("item", { name = command.name,
-				    node = node, jid = module:get_host() });
-				reply:up();
-			end
+module:hook("host-disco-items-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	if node ~= xmlns_cmd then
+		return;
+	end
+
+	local from = stanza.attr.from;
+	local admin = is_admin(from, stanza.attr.to);
+	local global_admin = is_admin(from);
+	local username, hostname = jid_split(from);
+	local nodes = array_collect(keys(commands)):sort();
+	for _, node in ipairs(nodes) do
+		local command = commands[node];
+		if (command.permission == "admin" and admin)
+		    or (command.permission == "global_admin" and global_admin)
+		    or (command.permission == "local_user" and hostname == module.host)
+		    or (command.permission == "user") then
+			reply:tag("item", { name = command.name,
+			    node = node, jid = module:get_host() });
+			reply:up();
 		end
-		origin.send(reply);
-		return true;
 	end
-end, 500);
+	event.exists = true;
+end);
 
 module:hook("iq/host/"..xmlns_cmd..":command", function (event)
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type == "set" then
 		local node = stanza.tags[1].attr.node
-		if commands[node] then
-			local admin = is_admin(stanza.attr.from, stanza.attr.to);
-			local global_admin = is_admin(stanza.attr.from);
-			if (commands[node].permission == "admin" and not admin)
-			    or (commands[node].permission == "global_admin" and not global_admin) then
+		local command = commands[node];
+		if command then
+			local from = stanza.attr.from;
+			local admin = is_admin(from, stanza.attr.to);
+			local global_admin = is_admin(from);
+			local username, hostname = jid_split(from);
+			if (command.permission == "admin" and not admin)
+			    or (command.permission == "global_admin" and not global_admin)
+			    or (command.permission == "local_user" and hostname ~= module.host) then
 				origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up()
 				    :add_child(commands[node]:cmdtag("canceled")
 					:tag("note", {type="error"}):text("You don't have permission to execute this command")));
--- a/plugins/mod_admin_adhoc.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_admin_adhoc.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -9,6 +9,7 @@
 local prosody = _G.prosody;
 local hosts = prosody.hosts;
 local t_concat = table.concat;
+local t_sort = table.sort;
 
 local module_host = module:get_host();
 
@@ -345,7 +346,7 @@
 		count = count + 1;
 		if fields.details then
 			for resource, session in pairs(user.sessions or {}) do
-				local status, priority = "unavailable", tostring(session.priority or "-");
+				local status, priority, ip = "unavailable", tostring(session.priority or "-"), session.ip or "<unknown>";
 				if session.presence then
 					status = session.presence:child_with_name("show");
 					if status then
@@ -354,13 +355,92 @@
 						status = "available";
 					end
 				end
-				users[#users+1] = " - "..resource..": "..status.."("..priority..")";
+				users[#users+1] = " - "..resource..": "..status.."("..priority.."), IP: ["..ip.."]";
 			end
 		end
 	end
 	return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} };
 end);
 
+-- Getting a list of S2S connections (this host)
+local list_s2s_this_result = dataforms_new {
+	title = "List of S2S connections on this host";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/s2s#list" };
+	{ name = "sessions", type = "text-multi", label = "Connections:" };
+	{ name = "num_in", type = "text-single", label = "#incomming connections:" };
+	{ name = "num_out", type = "text-single", label = "#outgoing connections:" };
+};
+
+local function session_flags(session, line)
+	line = line or {};
+
+	if session.id then
+		line[#line+1] = "["..session.id.."]"
+	else
+		line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
+	end
+
+	local flags = {};
+	if session.cert_identity_status == "valid" then
+		flags[#flags+1] = "authenticated";
+	end
+	if session.secure then
+		flags[#flags+1] = "encrypted";
+	end
+	if session.compressed then
+		flags[#flags+1] = "compressed)";
+	end
+	if session.smacks then
+		flags[#flags+1] = "sm";
+	end
+	if session.ip and session.ip:match(":") then
+		flags[#flags+1] = "IPv6";
+	end
+	line[#line+1] = "("..t_concat(flags, ", ")..")";
+
+	return t_concat(line, " ");
+end
+
+local function list_s2s_this_handler(self, data, state)
+	local count_in, count_out = 0, 0;
+	local s2s_list = {};
+
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+	for _, session in pairs(s2s_sessions) do
+		local remotehost, localhost, direction;
+		if session.direction == "outgoing" then
+			direction = "->";
+			count_out = count_out + 1;
+			remotehost, localhost = session.to_host or "?", session.from_host or "?";
+		else
+			direction = "<-";
+			count_in = count_in + 1;
+			remotehost, localhost = session.from_host or "?", session.to_host or "?";
+		end
+		local sess_lines = { r = remotehost,
+			session_flags(session, { "", direction, remotehost or "?" })};
+
+		if remotehost:match(module_host) or localhost:match(module_host) then
+			s2s_list[#s2s_list+1] = sess_lines;
+		end
+	end
+
+	t_sort(s2s_list, function(a, b)
+		return a.r < b.r;
+	end);
+
+	for i, sess_lines in ipairs(s2s_list) do
+		s2s_list[i] = sess_lines[1];
+	end
+
+	return { status = "completed", result = { layout = list_s2s_this_result; values = {
+		sessions = t_concat(s2s_list, "\n"),
+		num_in = tostring(count_in),
+		num_out = tostring(count_out)
+	} } };
+end
+
 -- Getting a list of loaded modules
 local list_modules_result = dataforms_new {
 	title = "List of loaded modules";
@@ -489,7 +569,7 @@
 	for _, host in pairs(hosts) do
 		loaded_modules:append(array(keys(host.modules)));
 	end
-	loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+	loaded_modules = array(set.new(loaded_modules):items()):sort();
 	return { module = loaded_modules };
 end, function(fields, err)
 	local is_global = false;
@@ -631,7 +711,7 @@
 	for _, host in pairs(hosts) do
 		loaded_modules:append(array(keys(host.modules)));
 	end
-	loaded_modules = array(keys(set.new(loaded_modules):items())):sort();
+	loaded_modules = array(set.new(loaded_modules):items()):sort();
 	return { module = loaded_modules };
 end, function(fields, err)
 	local is_global = false;
@@ -727,6 +807,7 @@
 local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin");
 local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin");
 local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users-list", get_online_users_command_handler, "admin");
+local list_s2s_this_desc = adhoc_new("List S2S connections", "http://prosody.im/protocol/s2s#list", list_s2s_this_handler, "admin");
 local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin");
 local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin");
 local globally_load_module_desc = adhoc_new("Globally load module", "http://prosody.im/protocol/modules#global-load", globally_load_module_handler, "global_admin");
@@ -747,6 +828,7 @@
 module:provides("adhoc", get_user_roster_desc);
 module:provides("adhoc", get_user_stats_desc);
 module:provides("adhoc", get_online_users_desc);
+module:provides("adhoc", list_s2s_this_desc);
 module:provides("adhoc", list_modules_desc);
 module:provides("adhoc", load_module_desc);
 module:provides("adhoc", globally_load_module_desc);
--- a/plugins/mod_admin_telnet.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_admin_telnet.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -17,7 +17,6 @@
 
 local prosody = _G.prosody;
 local hosts = prosody.hosts;
-local incoming_s2s = prosody.incoming_s2s;
 
 local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" };
 
@@ -28,6 +27,7 @@
 local cert_verify_identity = require "util.x509".verify_identity;
 local envload = require "util.envload".envload;
 local envloadfile = require "util.envload".envloadfile;
+local has_pposix, pposix = pcall(require, "util.pposix");
 
 local commands = module:shared("commands")
 local def_env = module:shared("env");
@@ -60,20 +60,20 @@
 			disconnect = function () conn:close(); end;
 			};
 	session.env = setmetatable({}, default_env_mt);
-	
+
 	-- Load up environment with helper objects
 	for name, t in pairs(def_env) do
 		if type(t) == "table" then
 			session.env[name] = setmetatable({ session = session }, { __index = t });
 		end
 	end
-	
+
 	return session;
 end
 
 function console:process_line(session, line)
 	local useglobalenv;
-	
+
 	if line:match("^>") then
 		line = line:gsub("^>", "");
 		useglobalenv = true;
@@ -87,9 +87,9 @@
 			return;
 		end
 	end
-	
+
 	session.env._ = line;
-	
+
 	local chunkname = "=console";
 	local env = (useglobalenv and redirect_output(_G, session)) or session.env or nil
 	local chunk, err = envload("return "..line, chunkname, env);
@@ -103,20 +103,20 @@
 			return;
 		end
 	end
-	
+
 	local ranok, taskok, message = pcall(chunk);
-	
+
 	if not (ranok or message or useglobalenv) and commands[line:lower()] then
 		commands[line:lower()](session, line);
 		return;
 	end
-	
+
 	if not ranok then
 		session.print("Fatal error while running command, it did not complete");
 		session.print("Error: "..taskok);
 		return;
 	end
-	
+
 	if not message then
 		session.print("Result: "..tostring(taskok));
 		return;
@@ -125,7 +125,7 @@
 		session.print("Message: "..tostring(message));
 		return;
 	end
-	
+
 	session.print("OK: "..tostring(message));
 end
 
@@ -155,6 +155,14 @@
 	session.partial_data = data:match("[^\n]+$");
 end
 
+function console_listener.onreadtimeout(conn)
+	local session = sessions[conn];
+	if session then
+		session.send("\0");
+		return true;
+	end
+end
+
 function console_listener.ondisconnect(conn, err)
 	local session = sessions[conn];
 	if session then
@@ -217,9 +225,11 @@
 		print [[c2s:show(jid) - Show all client sessions with the specified JID (or all if no JID given)]]
 		print [[c2s:show_insecure() - Show all unencrypted client connections]]
 		print [[c2s:show_secure() - Show all encrypted client connections]]
+		print [[c2s:show_tls() - Show TLS cipher info for encrypted sessions]]
 		print [[c2s:close(jid) - Close all sessions for the specified JID]]
 	elseif section == "s2s" then
 		print [[s2s:show(domain) - Show all s2s connections for the given domain (or all if no domain given)]]
+		print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]]
 		print [[s2s:close(from, to) - Close a connection from one domain to another]]
 		print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]]
 	elseif section == "module" then
@@ -313,8 +323,7 @@
 end
 
 function def_env.server:memory()
-	local pposix = require("util.pposix");
-	if not pposix.meminfo then
+	if not has_pposix or not pposix.meminfo then
 		return true, "Lua is using "..collectgarbage("count");
 	end
 	local mem, lua_mem = pposix.meminfo(), collectgarbage("count");
@@ -349,9 +358,9 @@
 
 function def_env.module:load(name, hosts, config)
 	local mm = require "modulemanager";
-	
+
 	hosts = get_hosts_set(hosts);
-	
+
 	-- Load the module for each host
 	local ok, err, count, mod = true, nil, 0, nil;
 	for host in hosts do
@@ -372,15 +381,15 @@
 			end
 		end
 	end
-	
-	return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));	
+
+	return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err));
 end
 
 function def_env.module:unload(name, hosts)
 	local mm = require "modulemanager";
 
 	hosts = get_hosts_set(hosts, name);
-	
+
 	-- Unload the module for each host
 	local ok, err, count = true, nil, 0;
 	for host in hosts do
@@ -438,7 +447,7 @@
 	if type(hosts) ~= "table" then
 		return false, "Please supply a host or a list of hosts you would like to see";
 	end
-	
+
 	local print = self.session.print;
 	for _, host in ipairs(hosts) do
 		print((host == "*" and "Global" or host)..":");
@@ -477,15 +486,57 @@
 	return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err);
 end
 
-def_env.hosts = {};
-function def_env.hosts:list()
-	for host, host_session in pairs(hosts) do
-		self.session.print(host);
+local function common_info(session, line)
+	if session.id then
+		line[#line+1] = "["..session.id.."]"
+	else
+		line[#line+1] = "["..session.type..(tostring(session):match("%x*$")).."]"
 	end
-	return true, "Done";
 end
 
-function def_env.hosts:add(name)
+local function session_flags(session, line)
+	line = line or {};
+	common_info(session, line);
+	if session.type == "c2s" then
+		local status, priority = "unavailable", tostring(session.priority or "-");
+		if session.presence then
+			status = session.presence:get_child_text("show") or "available";
+		end
+		line[#line+1] = status.."("..priority..")";
+	end
+	if session.cert_identity_status == "valid" then
+		line[#line+1] = "(authenticated)";
+	end
+	if 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 and session.ip:match(":") then
+		line[#line+1] = "(IPv6)";
+	end
+	return table.concat(line, " ");
+end
+
+local function tls_info(session, line)
+	line = line or {};
+	common_info(session, line);
+	if session.secure then
+		local sock = session.conn and session.conn.socket and session.conn:socket();
+		if sock and sock.info then
+			local info = sock:info();
+			line[#line+1] = ("(%s with %s)"):format(info.protocol, info.cipher);
+		else
+			line[#line+1] = "(cipher info unavailable)";
+		end
+	else
+		line[#line+1] = "(insecure)";
+	end
+	return table.concat(line, " ");
 end
 
 def_env.c2s = {};
@@ -506,13 +557,14 @@
 	show_c2s(function (jid, session)
 		if (not match_jid) or jid:match(match_jid) then
 			count = count + 1;
-		end		
+		end
 	end);
 	return true, "Total: "..count.." clients";
 end
 
-function def_env.c2s:show(match_jid)
+function def_env.c2s:show(match_jid, annotate)
 	local print, count = self.session.print, 0;
+	annotate = annotate or session_flags;
 	local curr_host;
 	show_c2s(function (jid, session)
 		if curr_host ~= session.host then
@@ -521,17 +573,8 @@
 		end
 		if (not match_jid) or jid:match(match_jid) then
 			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
-			end
-			print("   "..jid.." - "..status.."("..priority..")");
-		end		
+			print(annotate(session, { "  ", jid }));
+		end
 	end);
 	return true, "Total: "..count.." clients";
 end
@@ -542,7 +585,7 @@
 		if ((not match_jid) or jid:match(match_jid)) and not session.secure then
 			count = count + 1;
 			print(jid);
-		end		
+		end
 	end);
 	return true, "Total: "..count.." insecure client connections";
 end
@@ -553,11 +596,15 @@
 		if ((not match_jid) or jid:match(match_jid)) and session.secure then
 			count = count + 1;
 			print(jid);
-		end		
+		end
 	end);
 	return true, "Total: "..count.." secure client connections";
 end
 
+function def_env.c2s:show_tls(match_jid)
+	return self:show(match_jid, tls_info);
+end
+
 function def_env.c2s:close(match_jid)
 	local count = 0;
 	show_c2s(function (jid, session)
@@ -569,99 +616,87 @@
 	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)
-	local _print = self.session.print;
+function def_env.s2s:show(match_jid, annotate)
 	local print = self.session.print;
-	
+	annotate = annotate or session_flags;
+
 	local count_in, count_out = 0,0;
-	
-	for host, host_session in pairs(hosts) do
-		print = function (...) _print(host); _print(...); print = _print; end
-		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(session_flags(session, {"   ", host, "->", remotehost}));
-				if session.sendq then
-					print("        There are "..#session.sendq.." queued outgoing stanzas for this connection");
-				end
-				if session.type == "s2sout_unauthed" then
-					if session.connecting then
-						print("        Connection not yet established");
-						if not session.srv_hosts then
-							if not session.conn then
-								print("        We do not yet have a DNS answer for this host's SRV records");
-							else
-								print("        This host has no SRV records, using A record instead");
-							end
-						elseif session.srv_choice then
-							print("        We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
-							local srv_choice = session.srv_hosts[session.srv_choice];
-							print("        Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
+	local s2s_list = { };
+
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+	for _, session in pairs(s2s_sessions) do
+		local remotehost, localhost, direction;
+		if session.direction == "outgoing" then
+			direction = "->";
+			count_out = count_out + 1;
+			remotehost, localhost = session.to_host or "?", session.from_host or "?";
+		else
+			direction = "<-";
+			count_in = count_in + 1;
+			remotehost, localhost = session.from_host or "?", session.to_host or "?";
+		end
+		local sess_lines = { l = localhost, r = remotehost,
+			annotate(session, { "", direction, remotehost or "?" })};
+
+		if (not match_jid) or remotehost:match(match_jid) or localhost:match(match_jid) then
+			table.insert(s2s_list, sess_lines);
+			local print = function (s) table.insert(sess_lines, "        "..s); end
+			if session.sendq then
+				print("There are "..#session.sendq.." queued outgoing stanzas for this connection");
+			end
+			if session.type == "s2sout_unauthed" then
+				if session.connecting then
+					print("Connection not yet established");
+					if not session.srv_hosts then
+						if not session.conn then
+							print("We do not yet have a DNS answer for this host's SRV records");
+						else
+							print("This host has no SRV records, using A record instead");
 						end
-					elseif session.notopen then
-						print("        The <stream> has not yet been opened");
-					elseif not session.dialback_key then
-						print("        Dialback has not been initiated yet");
-					elseif session.dialback_key then
-						print("        Dialback has been requested, but no result received");
+					elseif session.srv_choice then
+						print("We are on SRV record "..session.srv_choice.." of "..#session.srv_hosts);
+						local srv_choice = session.srv_hosts[session.srv_choice];
+						print("Using "..(srv_choice.target or ".")..":"..(srv_choice.port or 5269));
 					end
+				elseif session.notopen then
+					print("The <stream> has not yet been opened");
+				elseif not session.dialback_key then
+					print("Dialback has not been initiated yet");
+				elseif session.dialback_key then
+					print("Dialback has been requested, but no result received");
 				end
 			end
-		end	
-		local subhost_filter = function (h)
-				return (match_jid and h:match(match_jid));
-			end
-		for session in pairs(incoming_s2s) do
-			if session.to_host == host and ((not match_jid) or host:match(match_jid)
-				or (session.from_host and session.from_host:match(match_jid))
-				-- 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(session_flags(session, {"   ", host, "<-", session.from_host or "(unknown)"}));
-				if session.type == "s2sin_unauthed" then
-						print("        Connection not yet authenticated");
-				end
+			if session.type == "s2sin_unauthed" then
+				print("Connection not yet authenticated");
+			elseif session.type == "s2sin" then
 				for name in pairs(session.hosts) do
 					if name ~= session.from_host then
-						print("        also hosts "..tostring(name));
+						print("also hosts "..tostring(name));
 					end
 				end
 			end
 		end
-		
-		print = _print;
 	end
-	
-	for session in pairs(incoming_s2s) do
-		if not session.to_host and ((not match_jid) or session.from_host and session.from_host:match(match_jid)) then
-			count_in = count_in + 1;
-			print("Other incoming s2s connections");
-			print("    (unknown) <- "..(session.from_host or "(unknown)"));			
-		end
+
+	-- Sort by local host, then remote host
+	table.sort(s2s_list, function(a,b)
+		if a.l == b.l then return a.r < b.r; end
+		return a.l < b.l;
+	end);
+	local lasthost;
+	for _, sess_lines in ipairs(s2s_list) do
+		if sess_lines.l ~= lasthost then print(sess_lines.l); lasthost=sess_lines.l end
+		for _, line in ipairs(sess_lines) do print(line); end
 	end
-	
 	return true, "Total: "..count_out.." outgoing, "..count_in.." incoming connections";
 end
 
+function def_env.s2s:show_tls(match_jid)
+	return self:show(match_jid, tls_info);
+end
+
 local function print_subject(print, subject)
 	for _, entry in ipairs(subject) do
 		print(
@@ -690,14 +725,9 @@
 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 and session or nil; 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 s2s_sessions = module:shared"/*/s2s/sessions";
+	local domain_sessions = set.new(array.collect(values(s2s_sessions)))
+		/function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end;
 	local cert_set = {};
 	for session in domain_sessions do
 		local conn = session.conn;
@@ -736,18 +766,18 @@
 	local domain_certs = array.collect(values(cert_set));
 	-- Phew. We now have a array of unique certificates presented by domain.
 	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 certs = cert_info.certs;
 		local cert = certs[1];
@@ -788,76 +818,38 @@
 
 function def_env.s2s:close(from, to)
 	local print, count = self.session.print, 0;
-	
-	if not (from and to) then
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+
+	local match_id;
+	if from and not to then
+		match_id, from = from;
+	elseif not to then
 		return false, "Syntax: s2s:close('from', 'to') - Closes all s2s sessions from 'from' to 'to'";
 	elseif from == to then
 		return false, "Both from and to are the same... you can't do that :)";
 	end
-	
-	if hosts[from] and not hosts[to] then
-		-- Is an outgoing connection
-		local session = hosts[from].s2sout[to];
-		if not session then
-			print("No outgoing connection from "..from.." to "..to)
-		else
+
+	for _, session in pairs(s2s_sessions) do
+		local id = session.type..tostring(session):match("[a-f0-9]+$");
+		if (match_id and match_id == id)
+		or (session.from_host == from and session.to_host == to) then
+			print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id));
 			(session.close or s2smanager.destroy_session)(session);
-			count = count + 1;
-			print("Closed outgoing session from "..from.." to "..to);
+			count = count + 1 ;
 		end
-	elseif hosts[to] and not hosts[from] then
-		-- Is an incoming connection
-		for session in pairs(incoming_s2s) do
-			if session.to_host == to and session.from_host == from then
-				(session.close or s2smanager.destroy_session)(session);
-				count = count + 1;
 			end
-		end
-		
-		if count == 0 then
-			print("No incoming connections from "..from.." to "..to);
-		else
-			print("Closed "..count.." incoming session"..((count == 1 and "") or "s").." from "..from.." to "..to);
-		end
-	elseif hosts[to] and hosts[from] then
-		return false, "Both of the hostnames you specified are local, there are no s2s sessions to close";
-	else
-		return false, "Neither of the hostnames you specified are being used on this server";
-	end
-	
 	return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s");
 end
 
 function def_env.s2s:closeall(host)
         local count = 0;
-
-        if not host or type(host) ~= "string" then return false, "wrong syntax: please use s2s:closeall('hostname.tld')"; end
-        if hosts[host] then
-                for session in pairs(incoming_s2s) do
-                        if session.to_host == host then
-                                (session.close or s2smanager.destroy_session)(session);
+	local s2s_sessions = module:shared"/*/s2s/sessions";
+	for _,session in pairs(s2s_sessions) do
+		if not host or session.from_host == host or session.to_host == host then
+			session:close();
                                 count = count + 1;
                         end
                 end
-                for _, session in pairs(hosts[host].s2sout) do
-                        (session.close or s2smanager.destroy_session)(session);
-                        count = count + 1;
-                end
-        else
-                for session in pairs(incoming_s2s) do
-			if session.from_host == host then
-				(session.close or s2smanager.destroy_session)(session);
-				count = count + 1;
-			end
-		end
-		for _, h in pairs(hosts) do
-			if h.s2sout[host] then
-				(h.s2sout[host].close or s2smanager.destroy_session)(h.s2sout[host]);
-				count = count + 1;
-			end
-		end
-        end
-
 	if count == 0 then return false, "No sessions to close.";
 	else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end
 end
@@ -874,9 +866,19 @@
 function def_env.host:list()
 	local print = self.session.print;
 	local i = 0;
+	local type;
 	for host in values(array.collect(keys(prosody.hosts)):sort()) do
 		i = i + 1;
-		print(host);
+		type = hosts[host].type;
+		if type == "local" then
+			print(host);
+		else
+			type = module:context(host):get_option_string("component_module", type);
+			if type ~= "component" then
+				type = type .. " component";
+			end
+			print(("%s (%s)"):format(host, type));
+		end
 	end
 	return true, i.." hosts";
 end
@@ -967,6 +969,20 @@
 	return setmetatable({ room = room_obj }, console_room_mt);
 end
 
+function def_env.muc:list(host)
+	local host_session = hosts[host];
+	if not host_session or not host_session.modules.muc then
+		return nil, "Please supply the address of a local MUC component";
+	end
+	local print = self.session.print;
+	local c = 0;
+	for name in keys(host_session.modules.muc.rooms) do
+		print(name);
+		c = c + 1;
+	end
+	return true, c.." rooms";
+end
+
 local um = require"core.usermanager";
 
 def_env.user = {};
@@ -1087,12 +1103,12 @@
 	local option = module:get_option("console_banner");
 	if option == nil or option == "full" or option == "graphic" then
 		session.print [[
-                   ____                \   /     _       
-                    |  _ \ _ __ ___  ___  _-_   __| |_   _ 
+                   ____                \   /     _
+                    |  _ \ _ __ ___  ___  _-_   __| |_   _
                     | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
                     |  __/| | | (_) \__ \ |_| | (_| | |_| |
                     |_|   |_|  \___/|___/\___/ \__,_|\__, |
-                    A study in simplicity            |___/ 
+                    A study in simplicity            |___/
 
 ]]
 	end
--- a/plugins/mod_announce.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_announce.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -39,22 +39,22 @@
 function handle_announcement(event)
 	local origin, stanza = event.origin, event.stanza;
 	local node, host, resource = jid.split(stanza.attr.to);
-	
+
 	if resource ~= "announce/online" then
 		return; -- Not an announcement
 	end
-	
+
 	if not is_admin(stanza.attr.from) then
 		-- Not an admin? Not allowed!
 		module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from);
 		return;
 	end
-	
+
 	module:log("info", "Sending server announcement to all online users");
 	local message = st.clone(stanza);
 	message.attr.type = "headline";
 	message.attr.from = host;
-	
+
 	local c = send_to_online(message, host);
 	module:log("info", "Announcement sent to %d online users", c);
 	return true;
@@ -83,9 +83,9 @@
 		module:log("info", "Sending server announcement to all online users");
 		local message = st.message({type = "headline"}, fields.announcement):up()
 			:tag("subject"):text(fields.subject or "Announcement");
-		
+
 		local count = send_to_online(message, data.to);
-		
+
 		module:log("info", "Announcement sent to %d online users", count);
 		return { status = "completed", info = ("Announcement sent to %d online users"):format(count) };
 	else
--- a/plugins/mod_auth_internal_hashed.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_auth_internal_hashed.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -7,12 +7,16 @@
 -- COPYING file in the source package for more information.
 --
 
-local log = require "util.logger".init("auth_internal_hashed");
+local max = math.max;
+
 local getAuthenticationDatabaseSHA1 = require "util.sasl.scram".getAuthenticationDatabaseSHA1;
 local usermanager = require "core.usermanager";
 local generate_uuid = require "util.uuid".generate;
 local new_sasl = require "util.sasl".new;
 
+local log = module._log;
+local host = module.host;
+
 local accounts = module:open_store("accounts");
 
 local to_hex;
@@ -37,14 +41,13 @@
 
 
 -- Default; can be set per-user
-local iteration_count = 4096;
+local default_iteration_count = 4096;
 
-local host = module.host;
 -- define auth provider
 local provider = {};
-log("debug", "initializing internal_hashed authentication provider for host '%s'", host);
 
 function provider.test_password(username, password)
+	log("debug", "test password for user '%s'", username);
 	local credentials = accounts:get(username) or {};
 
 	if credentials.password ~= nil and string.len(credentials.password) ~= 0 then
@@ -62,12 +65,12 @@
 	if credentials.iteration_count == nil or credentials.salt == nil or string.len(credentials.salt) == 0 then
 		return nil, "Auth failed. Stored salt and iteration count information is not complete.";
 	end
-	
+
 	local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, credentials.salt, credentials.iteration_count);
-	
+
 	local stored_key_hex = to_hex(stored_key);
 	local server_key_hex = to_hex(server_key);
-	
+
 	if valid and stored_key_hex == credentials.stored_key and server_key_hex == credentials.server_key then
 		return true;
 	else
@@ -76,14 +79,15 @@
 end
 
 function provider.set_password(username, password)
+	log("debug", "set_password for username '%s'", username);
 	local account = accounts:get(username);
 	if account then
-		account.salt = account.salt or generate_uuid();
-		account.iteration_count = account.iteration_count or iteration_count;
+		account.salt = generate_uuid();
+		account.iteration_count = max(account.iteration_count or 0, default_iteration_count);
 		local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, account.salt, account.iteration_count);
 		local stored_key_hex = to_hex(stored_key);
 		local server_key_hex = to_hex(server_key);
-		
+
 		account.stored_key = stored_key_hex
 		account.server_key = server_key_hex
 
@@ -96,7 +100,7 @@
 function provider.user_exists(username)
 	local account = accounts:get(username);
 	if not account then
-		log("debug", "account not found for username '%s' at host '%s'", username, host);
+		log("debug", "account not found for username '%s'", username);
 		return nil, "Auth failed. Invalid username";
 	end
 	return true;
@@ -111,10 +115,10 @@
 		return accounts:set(username, {});
 	end
 	local salt = generate_uuid();
-	local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count);
+	local valid, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, default_iteration_count);
 	local stored_key_hex = to_hex(stored_key);
 	local server_key_hex = to_hex(server_key);
-	return accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = iteration_count});
+	return accounts:set(username, {stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = default_iteration_count});
 end
 
 function provider.delete_user(username)
@@ -134,7 +138,7 @@
 				credentials = accounts:get(username);
 				if not credentials then return; end
 			end
-			
+
 			local stored_key, server_key, iteration_count, salt = credentials.stored_key, credentials.server_key, credentials.iteration_count, credentials.salt;
 			stored_key = stored_key and from_hex(stored_key);
 			server_key = server_key and from_hex(server_key);
@@ -143,6 +147,6 @@
 	};
 	return new_sasl(host, testpass_authentication_profile);
 end
-	
+
 module:provides("auth", provider);
 
--- a/plugins/mod_auth_internal_plain.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_auth_internal_plain.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -16,10 +16,9 @@
 
 -- define auth provider
 local provider = {};
-log("debug", "initializing internal_plain authentication provider for host '%s'", host);
 
 function provider.test_password(username, password)
-	log("debug", "test password for user %s at host %s", username, host);
+	log("debug", "test password for user '%s'", username);
 	local credentials = accounts:get(username) or {};
 
 	if password == credentials.password then
@@ -30,11 +29,12 @@
 end
 
 function provider.get_password(username)
-	log("debug", "get_password for username '%s' at host '%s'", username, host);
+	log("debug", "get_password for username '%s'", username);
 	return (accounts:get(username) or {}).password;
 end
 
 function provider.set_password(username, password)
+	log("debug", "set_password for username '%s'", username);
 	local account = accounts:get(username);
 	if account then
 		account.password = password;
@@ -46,7 +46,7 @@
 function provider.user_exists(username)
 	local account = accounts:get(username);
 	if not account then
-		log("debug", "account not found for username '%s' at host '%s'", username, host);
+		log("debug", "account not found for username '%s'", username);
 		return nil, "Auth failed. Invalid username";
 	end
 	return true;
@@ -76,6 +76,6 @@
 	};
 	return new_sasl(host, getpass_authentication_profile);
 end
-	
+
 module:provides("auth", provider);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_blocklist.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,275 @@
+-- Prosody IM
+-- Copyright (C) 2009-2010 Matthew Wild
+-- Copyright (C) 2009-2010 Waqas Hussain
+-- Copyright (C) 2014 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- This module implements XEP-0191: Blocking Command
+--
+
+local user_exists = require"core.usermanager".user_exists;
+local is_contact_subscribed = require"core.rostermanager".is_contact_subscribed;
+local st = require"util.stanza";
+local st_error_reply = st.error_reply;
+local jid_prep, jid_split = import("util.jid", "prep", "split");
+
+local host = module.host;
+local storage = module:open_store();
+local sessions = prosody.hosts[host].sessions;
+
+-- Cache of blocklists used since module was loaded
+local cache = {};
+if module:get_option_boolean("blocklist_weak_cache") then
+	-- Lower memory usage, more IO and latency
+	setmetatable(cache, { __mode = "v" });
+end
+
+local null_blocklist = {};
+
+module:add_feature("urn:xmpp:blocking");
+
+local function set_blocklist(username, blocklist)
+	local ok, err = storage:set(username, blocklist);
+	if not ok then
+		return ok, err;
+	end
+	-- Successful save, update the cache
+	cache[username] = blocklist;
+	return true;
+end
+
+-- Migrates from the old mod_privacy storage
+local function migrate_privacy_list(username)
+	local migrated_data = { [false] = "not empty" };
+	local legacy_data = module:open_store("privacy"):get(username);
+	if legacy_data and legacy_data.lists and legacy_data.default then
+		legacy_data = legacy_data.lists[legacy_data.default];
+		legacy_data = legacy_data and legacy_data.items;
+	else
+		return migrated_data;
+	end
+	if legacy_data then
+		module:log("info", "Migrating blocklist from mod_privacy storage for user '%s'", username);
+		local item, jid;
+		for i = 1, #legacy_data do
+			item = legacy_data[i];
+			if item.type == "jid" and item.action == "deny" then
+				jid = jid_prep(item.value);
+				if not jid then
+					module:log("warn", "Invalid JID in privacy store for user '%s' not migrated: %s", username, tostring(item.value));
+				else
+					migrated_data[jid] = true;
+				end
+			end
+		end
+	end
+	set_blocklist(username, migrated_data);
+	return migrated_data;
+end
+
+local function get_blocklist(username)
+	local blocklist = cache[username];
+	if not blocklist then
+		if not user_exists(username, host) then
+			return null_blocklist;
+		end
+		blocklist = storage:get(username);
+		if not blocklist then
+			blocklist = migrate_privacy_list(username);
+		end
+		cache[username] = blocklist;
+	end
+	return blocklist;
+end
+
+module:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	local username = origin.username;
+	local reply = st.reply(stanza):tag("blocklist", { xmlns = "urn:xmpp:blocking" });
+	local blocklist = get_blocklist(username);
+	for jid in pairs(blocklist) do
+		if jid then
+			reply:tag("item", { jid = jid }):up();
+		end
+	end
+	origin.interested_blocklist = true; -- Gets notified about changes
+	return origin.send(reply);
+end);
+
+-- Add or remove some jid(s) from the blocklist
+-- We want this to be atomic and not do a partial update
+local function edit_blocklist(event)
+	local origin, stanza = event.origin, event.stanza;
+	local username = origin.username;
+	local action = stanza.tags[1];
+	local new = {};
+
+	local jid;
+	for item in action:childtags("item") do
+		jid = jid_prep(item.attr.jid);
+		if not jid then
+			return origin.send(st_error_reply(stanza, "modify", "jid-malformed"));
+		end
+		item.attr.jid = jid; -- echo back prepped
+		new[jid] = is_contact_subscribed(username, host, jid) or false;
+	end
+
+	local mode = action.name == "block" or nil;
+
+	if mode and not next(new) then
+		-- <block/> element does not contain at least one <item/> child element
+		return origin.send(st_error_reply(stanza, "modify", "bad-request"));
+	end
+
+	local blocklist = get_blocklist(username);
+
+	local new_blocklist = {};
+
+	if mode or next(new) then
+		for jid in pairs(blocklist) do
+			new_blocklist[jid] = true;
+		end
+		for jid in pairs(new) do
+			new_blocklist[jid] = mode;
+		end
+		-- else empty the blocklist
+	end
+	new_blocklist[false] = "not empty"; -- In order to avoid doing the migration thing twice
+
+	local ok, err = set_blocklist(username, new_blocklist);
+	if ok then
+		origin.send(st.reply(stanza));
+	else
+		return origin.send(st_error_reply(stanza, "wait", "internal-server-error", err));
+	end
+
+	if mode then
+		for jid, in_roster in pairs(new) do
+			if not blocklist[jid] and in_roster and sessions[username] then
+				for _, session in pairs(sessions[username].sessions) do
+					if session.presence then
+						module:send(st.presence({ type = "unavailable", to = jid, from = session.full_jid }));
+					end
+				end
+			end
+		end
+	end
+	if sessions[username] then
+		local blocklist_push = st.iq({ type = "set", id = "blocklist-push" })
+			:add_child(action); -- I am lazy
+
+		for _, session in pairs(sessions[username].sessions) do
+			if session.interested_blocklist then
+				blocklist_push.attr.to = session.full_jid;
+				session.send(blocklist_push);
+			end
+		end
+	end
+
+	return true;
+end
+
+module:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist);
+module:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist);
+
+-- Cache invalidation, solved!
+module:hook_global("user-deleted", function (event)
+	if event.host == host then
+		cache[event.username] = nil;
+	end
+end);
+
+-- Buggy clients
+module:hook("iq-error/self/blocklist-push", function (event)
+	local type, condition, text = event.stanza:get_error();
+	(event.origin.log or module._log)("warn", "Client returned an error in response to notification from mod_%s: %s%s%s", module.name, condition, text and ": " or "", text or "");
+	return true;
+end);
+
+local function is_blocked(user, jid)
+	local blocklist = cache[user] or get_blocklist(user);
+	if blocklist[jid] then return true; end
+	local node, host = jid_split(jid);
+	return blocklist[host] or node and blocklist[node..'@'..host];
+end
+
+-- Event handlers for bouncing or dropping stanzas
+local function drop_stanza(event)
+	local stanza = event.stanza;
+	local attr = stanza.attr;
+	local to, from = attr.to, attr.from;
+	to = to and jid_split(to);
+	if to and from then
+		return is_blocked(to, from);
+	end
+end
+
+local function bounce_stanza(event)
+	local origin, stanza = event.origin, event.stanza;
+	if drop_stanza(event) then
+		return origin.send(st_error_reply(stanza, "cancel", "service-unavailable"));
+	end
+end
+
+local function bounce_iq(event)
+	local type = event.stanza.attr.type;
+	if type == "set" or type == "get" then
+		return bounce_stanza(event);
+	end
+	return drop_stanza(event); -- result or error
+end
+
+local function bounce_message(event)
+	local type = event.stanza.attr.type;
+	if type == "chat" or not type or type == "normal" then
+		return bounce_stanza(event);
+	end
+	return drop_stanza(event); -- drop headlines, groupchats etc
+end
+
+local function drop_outgoing(event)
+	local origin, stanza = event.origin, event.stanza;
+	local username = origin.username or jid_split(stanza.attr.from);
+	if not username then return end
+	local to = stanza.attr.to;
+	if to then return is_blocked(username, to); end
+	-- nil 'to' means a self event, don't bock those
+end
+
+local function bounce_outgoing(event)
+	local origin, stanza = event.origin, event.stanza;
+	local type = stanza.attr.type;
+	if type == "error" or stanza.name == "iq" and type == "result" then
+		return drop_outgoing(event);
+	end
+	if drop_outgoing(event) then
+		return origin.send(st_error_reply(stanza, "cancel", "not-acceptable", "You have blocked this JID")
+			:tag("blocked", { xmlns = "urn:xmpp:blocking:errors" }));
+	end
+end
+
+-- Hook all the events!
+local prio_in, prio_out = 100, 100;
+module:hook("presence/bare", drop_stanza, prio_in);
+module:hook("presence/full", drop_stanza, prio_in);
+
+module:hook("message/bare", bounce_message, prio_in);
+module:hook("message/full", bounce_message, prio_in);
+
+module:hook("iq/bare", bounce_iq, prio_in);
+module:hook("iq/full", bounce_iq, prio_in);
+
+module:hook("pre-message/bare", bounce_outgoing, prio_out);
+module:hook("pre-message/full", bounce_outgoing, prio_out);
+module:hook("pre-message/host", bounce_outgoing, prio_out);
+
+module:hook("pre-presence/bare", drop_outgoing, prio_out);
+module:hook("pre-presence/full", drop_outgoing, prio_out);
+module:hook("pre-presence/host", drop_outgoing, prio_out);
+
+module:hook("pre-iq/bare", bounce_outgoing, prio_out);
+module:hook("pre-iq/full", bounce_outgoing, prio_out);
+module:hook("pre-iq/host", bounce_outgoing, prio_out);
+
--- a/plugins/mod_bosh.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_bosh.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -22,6 +22,7 @@
 local math_min = math.min;
 local xpcall, tostring, type = xpcall, tostring, type;
 local traceback = debug.traceback;
+local runner = require"util.async".runner;
 
 local xmlns_streams = "http://etherx.jabber.org/streams";
 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
@@ -37,24 +38,10 @@
 local bosh_max_wait = module:get_option_number("bosh_max_wait", 120);
 
 local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
-
-local default_headers = { ["Content-Type"] = "text/xml; charset=utf-8" };
-
 local cross_domain = module:get_option("cross_domain_bosh", false);
-if cross_domain then
-	default_headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
-	default_headers["Access-Control-Allow-Headers"] = "Content-Type";
-	default_headers["Access-Control-Max-Age"] = "7200";
 
-	if cross_domain == true then
-		default_headers["Access-Control-Allow-Origin"] = "*";
-	elseif type(cross_domain) == "table" then
-		cross_domain = table.concat(cross_domain, ", ");
-	end
-	if type(cross_domain) == "string" then
-		default_headers["Access-Control-Allow-Origin"] = cross_domain;
-	end
-end
+if cross_domain == true then cross_domain = "*"; end
+if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end
 
 local trusted_proxies = module:get_option_set("trusted_proxies", {"127.0.0.1"})._items;
 
@@ -79,7 +66,7 @@
 local sessions, inactive_sessions = module:shared("sessions", "inactive_sessions");
 
 -- Used to respond to idle sessions (those with waiting requests)
-local waiting_requests = {};
+local waiting_requests = module:shared("waiting_requests");
 function on_destroy_request(request)
 	log("debug", "Request destroyed: %s", tostring(request));
 	waiting_requests[request] = nil;
@@ -92,7 +79,7 @@
 				break;
 			end
 		end
-		
+
 		-- If this session now has no requests open, mark it as inactive
 		local max_inactive = session.bosh_max_inactive;
 		if max_inactive and #requests == 0 then
@@ -102,11 +89,20 @@
 	end
 end
 
-function handle_OPTIONS(request)
-	local headers = {};
-	for k,v in pairs(default_headers) do headers[k] = v; end
-	headers["Content-Type"] = nil;
-	return { headers = headers, body = "" };
+local function set_cross_domain_headers(response)
+	local headers = response.headers;
+	headers.access_control_allow_methods = "GET, POST, OPTIONS";
+	headers.access_control_allow_headers = "Content-Type";
+	headers.access_control_max_age = "7200";
+	headers.access_control_allow_origin = cross_domain;
+	return response;
+end
+
+function handle_OPTIONS(event)
+	if cross_domain and event.request.headers.origin then
+		set_cross_domain_headers(event.response);
+	end
+	return "";
 end
 
 function handle_POST(event)
@@ -119,14 +115,24 @@
 	local context = { request = request, response = response, notopen = true };
 	local stream = new_xmpp_stream(context, stream_callbacks);
 	response.context = context;
-	
+
+	local headers = response.headers;
+	headers.content_type = "text/xml; charset=utf-8";
+
+	if cross_domain and event.request.headers.origin then
+		set_cross_domain_headers(response);
+	end
+
 	-- stream:feed() calls the stream_callbacks, so all stanzas in
 	-- the body are processed in this next line before it returns.
 	-- In particular, the streamopened() stream callback is where
 	-- much of the session logic happens, because it's where we first
 	-- get to see the 'sid' of this request.
-	stream:feed(body);
-	
+	if not stream:feed(body) then
+		module:log("warn", "Error parsing BOSH payload")
+		return 400;
+	end
+
 	-- Stanzas (if any) in the request have now been processed, and
 	-- we take care of the high-level BOSH logic here, including
 	-- giving a response or putting the request "on hold".
@@ -141,9 +147,6 @@
 		local r = session.requests;
 		log("debug", "Session %s has %d out of %d requests open", context.sid, #r, session.bosh_hold);
 		log("debug", "and there are %d things in the send_buffer:", #session.send_buffer);
-		for i, thing in ipairs(session.send_buffer) do
-			log("debug", "    %s", tostring(thing));
-		end
 		if #r > session.bosh_hold then
 			-- We are holding too many requests, send what's in the buffer,
 			log("debug", "We are holding too many requests, so...");
@@ -162,7 +165,7 @@
 			session.send_buffer = {};
 			session.send(resp);
 		end
-		
+
 		if not response.finished then
 			-- We're keeping this request open, to respond later
 			log("debug", "Have nothing to say, so leaving request unanswered for now");
@@ -170,7 +173,7 @@
 				waiting_requests[response] = os_time() + session.bosh_wait;
 			end
 		end
-		
+
 		if session.bosh_terminate then
 			session.log("debug", "Closing session with %d requests open", #session.requests);
 			session:close();
@@ -179,6 +182,8 @@
 			return true; -- Inform http server we shall reply later
 		end
 	end
+	module:log("warn", "Unable to associate request with a session (incomplete request?)");
+	return 400;
 end
 
 
@@ -188,10 +193,10 @@
 
 local function bosh_close_stream(session, reason)
 	(session.log or log)("info", "BOSH client disconnected");
-	
+
 	local close_reply = st.stanza("body", { xmlns = xmlns_bosh, type = "terminate",
 		["xmlns:stream"] = xmlns_streams });
-	
+
 
 	if reason then
 		close_reply.attr.condition = "remote-stream-error";
@@ -217,14 +222,15 @@
 
 	local response_body = tostring(close_reply);
 	for _, held_request in ipairs(session.requests) do
-		held_request.headers = default_headers;
 		held_request:send(response_body);
 	end
-	sessions[session.sid]  = nil;
+	sessions[session.sid] = nil;
 	inactive_sessions[session] = nil;
 	sm_destroy_session(session);
 end
 
+local runner_callbacks = { };
+
 -- Handle the <body> tag in the request payload.
 function stream_callbacks.streamopened(context, attr)
 	local request, response = context.request, context.response;
@@ -233,7 +239,7 @@
 	if not sid then
 		-- New session request
 		context.notopen = nil; -- Signals that we accept this opening tag
-		
+
 		-- TODO: Sanity checks here (rid, to, known host, etc.)
 		if not hosts[attr.to] then
 			-- Unknown host
@@ -243,7 +249,7 @@
 			response:send(tostring(close_reply));
 			return;
 		end
-		
+
 		-- New session
 		sid = new_uuid();
 		local session = {
@@ -256,9 +262,13 @@
 			ip = get_ip_from_request(request);
 		};
 		sessions[sid] = session;
-		
+
+		session.thread = runner(function (stanza)
+			session:dispatch_stanza(stanza);
+		end, runner_callbacks, session);
+
 		local filter = initialize_filters(session);
-		
+
 		session.log("debug", "BOSH session created for request from %s", session.ip);
 		log("info", "New BOSH session, assigned it sid '%s'", sid);
 
@@ -279,7 +289,6 @@
 			local oldest_request = r[1];
 			if oldest_request and not session.bosh_processing then
 				log("debug", "We have an open request, so sending on that");
-				oldest_request.headers = default_headers;
 				local body_attr = { xmlns = "http://jabber.org/protocol/httpbind",
 					["xmlns:stream"] = "http://etherx.jabber.org/streams";
 					type = session.bosh_terminate and "terminate" or nil;
@@ -306,17 +315,16 @@
 		end
 		request.sid = sid;
 	end
-	
+
 	local session = sessions[sid];
 	if not session then
 		-- Unknown sid
 		log("info", "Client tried to use sid '%s' which we don't know about", sid);
-		response.headers = default_headers;
 		response:send(tostring(st.stanza("body", { xmlns = xmlns_bosh, type = "terminate", condition = "item-not-found" })));
 		context.notopen = nil;
 		return;
 	end
-	
+
 	if session.rid then
 		local rid = tonumber(attr.rid);
 		local diff = rid - session.rid;
@@ -333,7 +341,7 @@
 		end
 		session.rid = rid;
 	end
-	
+
 	if attr.type == "terminate" then
 		-- Client wants to end this session, which we'll do
 		-- after processing any stanzas in this request
@@ -348,13 +356,17 @@
 	if session.notopen then
 		local features = st.stanza("stream:features");
 		hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
-		fire_event("stream-features", session, features);
-		session.send(tostring(features));
+		session.send(features);
 		session.notopen = nil;
 	end
 end
 
 local function handleerr(err) log("error", "Traceback[bosh]: %s", traceback(tostring(err), 2)); end
+
+function runner_callbacks:error(err)
+	return handleerr(err);
+end
+
 function stream_callbacks.handlestanza(context, stanza)
 	if context.ignore then return; end
 	log("debug", "BOSH stanza received: %s\n", stanza:top_tag());
@@ -364,14 +376,12 @@
 			stanza.attr.xmlns = nil;
 		end
 		stanza = session.filter("stanzas/in", stanza);
-		if stanza then
-			return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
-		end
+		session.thread:run(stanza);
 	end
 end
 
-function stream_callbacks.streamclosed(request)
-	local session = sessions[request.sid];
+function stream_callbacks.streamclosed(context)
+	local session = sessions[context.sid];
 	if session then
 		session.bosh_processing = false;
 		if #session.send_buffer > 0 then
@@ -384,12 +394,11 @@
 	log("debug", "Error parsing BOSH request payload; %s", error);
 	if not context.sid then
 		local response = context.response;
-		response.headers = default_headers;
 		response.status_code = 400;
 		response:send();
 		return;
 	end
-	
+
 	local session = sessions[context.sid];
 	if error == "stream-error" then -- Remote stream error, we close normally
 		session:close();
@@ -398,7 +407,7 @@
 	end
 end
 
-local dead_sessions = {};
+local dead_sessions = module:shared("dead_sessions");
 function on_timer()
 	-- log("debug", "Checking for requests soon to timeout...");
 	-- Identify requests timing out within the next few seconds
@@ -413,7 +422,7 @@
 			end
 		end
 	end
-	
+
 	now = now - 3;
 	local n_dead_sessions = 0;
 	for session, close_after in pairs(inactive_sessions) do
--- a/plugins/mod_c2s.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_c2s.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -15,9 +15,10 @@
 local st = require "util.stanza";
 local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session;
 local uuid_generate = require "util.uuid".generate;
+local runner = require "util.async".runner;
 
 local xpcall, tostring, type = xpcall, tostring, type;
-local traceback = debug.traceback;
+local t_insert, t_remove = table.insert, table.remove;
 
 local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams";
 
@@ -31,12 +32,12 @@
 local core_process_stanza = prosody.core_process_stanza;
 local hosts = prosody.hosts;
 
-local stream_callbacks = { default_ns = "jabber:client", handlestanza = core_process_stanza };
+local stream_callbacks = { default_ns = "jabber:client" };
 local listener = {};
+local runner_callbacks = {};
 
 --- Stream events handlers
 local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
-local default_stream_attr = { ["xmlns:stream"] = "http://etherx.jabber.org/streams", xmlns = stream_callbacks.default_ns, version = "1.0", id = "" };
 
 function stream_callbacks.streamopened(session, attr)
 	local send = session.send;
@@ -50,15 +51,13 @@
 	session.streamid = uuid_generate();
 	(session.log or session)("debug", "Client sent opening <stream:stream> to %s", session.host);
 
-	if not hosts[session.host] or not hosts[session.host].users then
+	if not hosts[session.host] or not hosts[session.host].modules.c2s then
 		-- We don't serve this host...
 		session:close{ condition = "host-unknown", text = "This server does not serve "..tostring(session.host)};
 		return;
 	end
 
-	send("<?xml version='1.0'?>"..st.stanza("stream:stream", {
-		xmlns = 'jabber:client', ["xmlns:stream"] = 'http://etherx.jabber.org/streams';
-		id = session.streamid, from = session.host, version = '1.0', ["xml:lang"] = 'en' }):top_tag());
+	session:open_stream();
 
 	(session.log or log)("debug", "Sent reply <stream:stream> to client");
 	session.notopen = nil;
@@ -67,20 +66,21 @@
 	-- since we now have a new stream header, session is secured
 	if session.secure == false then
 		session.secure = true;
+		session.encrypted = true;
 
-		-- Check if TLS compression is used
 		local sock = session.conn:socket();
 		if sock.info then
-			session.compressed = sock:info"compression";
-		elseif sock.compression then
-			session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+			local info = sock:info();
+			(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
+			session.compressed = info.compression;
+		else
+			(session.log or log)("info", "Stream encrypted");
+			session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg
 		end
 	end
 
 	local features = st.stanza("stream:features");
 	hosts[session.host].events.fire_event("stream-features", { origin = session, features = features });
-	module:fire_event("stream-features", session, features);
-
 	send(features);
 end
 
@@ -116,12 +116,9 @@
 	end
 end
 
-local function handleerr(err) log("error", "Traceback[c2s]: %s", traceback(tostring(err), 2)); end
 function stream_callbacks.handlestanza(session, stanza)
 	stanza = session.filter("stanzas/in", stanza);
-	if stanza then
-		return xpcall(function () return core_process_stanza(session, stanza) end, handleerr);
-	end
+	session.thread:run(stanza);
 end
 
 --- Session methods
@@ -129,8 +126,7 @@
 	local log = session.log or log;
 	if session.conn then
 		if session.notopen then
-			session.send("<?xml version='1.0'?>");
-			session.send(st.stanza("stream:stream", default_stream_attr):top_tag());
+			session:open_stream();
 		end
 		if reason then -- nil == no err, initiated by us, false == initiated by client
 			local stream_error = st.stanza("stream:error");
@@ -153,12 +149,12 @@
 			log("debug", "Disconnecting client, <stream:error> is: %s", stream_error);
 			session.send(stream_error);
 		end
-		
+
 		session.send("</stream:stream>");
 		function session.send() return false; end
-		
+
 		local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
-		session.log("info", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+		session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
 
 		-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
 		local conn = session.conn;
@@ -188,16 +184,29 @@
 	end
 end, 200);
 
+function runner_callbacks:ready()
+	self.data.conn:resume();
+end
+
+function runner_callbacks:waiting()
+	self.data.conn:pause();
+end
+
+function runner_callbacks:error(err)
+	(self.data.log or log)("error", "Traceback[c2s]: %s", err);
+end
+
 --- Port listener
 function listener.onconnect(conn)
 	local session = sm_new_session(conn);
 	sessions[conn] = session;
-	
+
 	session.log("info", "Client connected");
-	
+
 	-- Client is using legacy SSL (otherwise mod_tls sets this flag)
 	if conn:ssl() then
 		session.secure = true;
+		session.encrypted = true;
 
 		-- Check if TLS compression is used
 		local sock = conn:socket();
@@ -207,34 +216,41 @@
 			session.compressed = sock:compression(); --COMPAT mw/luasec-hg
 		end
 	end
-	
+
 	if opt_keepalives then
 		conn:setoption("keepalive", opt_keepalives);
 	end
-	
+
 	session.close = session_close;
-	
+
 	local stream = new_xmpp_stream(session, stream_callbacks);
 	session.stream = stream;
 	session.notopen = true;
-	
+
 	function session.reset_stream()
 		session.notopen = true;
 		session.stream:reset();
 	end
-	
+
+	session.thread = runner(function (stanza)
+		core_process_stanza(session, stanza);
+	end, runner_callbacks, session);
+
 	local filter = session.filter;
 	function session.data(data)
+		-- Parse the data, which will store stanzas in session.pending_stanzas
+		if data then
 		data = filter("bytes/in", data);
 		if data then
 			local ok, err = stream:feed(data);
-			if ok then return; end
-			log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
-			session:close("not-well-formed");
+				if not ok then
+					log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+					session:close("not-well-formed");
+				end
+			end
 		end
 	end
 
-	
 	if c2s_timeout then
 		add_task(c2s_timeout, function ()
 			if session.type == "c2s_unauthed" then
@@ -262,14 +278,27 @@
 	end
 end
 
+function listener.onreadtimeout(conn)
+	local session = sessions[conn];
+	if session then
+		return (hosts[session.host] or prosody).events.fire_event("c2s-read-timeout", { session = session });
+	end
+end
+
+local function keepalive(event)
+	return event.session.send(' ');
+end
+
 function listener.associate_session(conn, session)
 	sessions[conn] = session;
 end
 
-function listener.ondetach(conn)
-	sessions[conn] = nil;
+function module.add_host(module)
+	module:hook("c2s-read-timeout", keepalive, -1);
 end
 
+module:hook("c2s-read-timeout", keepalive, -1);
+
 module:hook("server-stopping", function(event)
 	local reason = event.reason;
 	for _, session in pairs(sessions) do
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_carbons.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,111 @@
+-- XEP-0280: Message Carbons implementation for Prosody
+-- Copyright (C) 2011 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local xmlns_carbons = "urn:xmpp:carbons:2";
+local xmlns_forward = "urn:xmpp:forward:0";
+local full_sessions, bare_sessions = full_sessions, bare_sessions;
+
+local function toggle_carbons(event)
+	local origin, stanza = event.origin, event.stanza;
+	local state = stanza.tags[1].name;
+	module:log("debug", "%s %sd carbons", origin.full_jid, state);
+	origin.want_carbons = state == "enable" and stanza.tags[1].attr.xmlns;
+	return origin.send(st.reply(stanza));
+end
+module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons);
+module:hook("iq-set/self/"..xmlns_carbons..":enable", toggle_carbons);
+
+local function message_handler(event, c2s)
+	local origin, stanza = event.origin, event.stanza;
+	local orig_type = stanza.attr.type;
+	local orig_from = stanza.attr.from;
+	local orig_to = stanza.attr.to;
+	
+	if not (orig_type == nil
+			or orig_type == "normal"
+			or orig_type == "chat") then
+		return -- No carbons for messages of type error or headline
+	end
+
+	-- Stanza sent by a local client
+	local bare_jid = jid_bare(orig_from);
+	local target_session = origin;
+	local top_priority = false;
+	local user_sessions = bare_sessions[bare_jid];
+
+	-- Stanza about to be delivered to a local client
+	if not c2s then
+		bare_jid = jid_bare(orig_to);
+		target_session = full_sessions[orig_to];
+		user_sessions = bare_sessions[bare_jid];
+		if not target_session and user_sessions then
+			-- The top resources will already receive this message per normal routing rules,
+			-- so we are going to skip them in order to avoid sending duplicated messages.
+			local top_resources = user_sessions.top_resources;
+			top_priority = top_resources and top_resources[1].priority
+		end
+	end
+
+	if not user_sessions then
+		module:log("debug", "Skip carbons for offline user");
+		return -- No use in sending carbons to an offline user
+	end
+
+	if stanza:get_child("private", xmlns_carbons) then
+		if not c2s then
+			stanza:maptags(function(tag)
+				if not ( tag.attr.xmlns == xmlns_carbons and tag.name == "private" ) then
+					return tag;
+				end
+			end);
+		end
+		module:log("debug", "Message tagged private, ignoring");
+		return
+	elseif stanza:get_child("no-copy", "urn:xmpp:hints") then
+		module:log("debug", "Message has no-copy hint, ignoring");
+		return
+	elseif stanza:get_child("x", "http://jabber.org/protocol/muc#user") then
+		module:log("debug", "MUC PM, ignoring");
+		return
+	end
+
+	-- Create the carbon copy and wrap it as per the Stanza Forwarding XEP
+	local copy = st.clone(stanza);
+	copy.attr.xmlns = "jabber:client";
+	local carbon = st.message{ from = bare_jid, type = orig_type, }
+		:tag(c2s and "sent" or "received", { xmlns = xmlns_carbons })
+			:tag("forwarded", { xmlns = xmlns_forward })
+				:add_child(copy):reset();
+
+	user_sessions = user_sessions and user_sessions.sessions;
+	for _, session in pairs(user_sessions) do
+		-- Carbons are sent to resources that have enabled it
+		if session.want_carbons
+		-- but not the resource that sent the message, or the one that it's directed to
+		and session ~= target_session
+		-- and isn't among the top resources that would receive the message per standard routing rules
+		and (c2s or session.priority ~= top_priority) then
+			carbon.attr.to = session.full_jid;
+			module:log("debug", "Sending carbon to %s", session.full_jid);
+			session.send(carbon);
+		end
+	end
+end
+
+local function c2s_message_handler(event)
+	return message_handler(event, true)
+end
+
+-- Stanzas sent by local clients
+module:hook("pre-message/host", c2s_message_handler, 1);
+module:hook("pre-message/bare", c2s_message_handler, 1);
+module:hook("pre-message/full", c2s_message_handler, 1);
+-- Stanzas to local clients
+module:hook("message/bare", message_handler, 1);
+module:hook("message/full", message_handler, 1);
+
+module:add_feature(xmlns_carbons);
--- a/plugins/mod_component.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_component.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -33,7 +33,7 @@
 	if module:get_host_type() ~= "component" then
 		error("Don't load mod_component manually, it should be for a component, please see http://prosody.im/doc/components", 0);
 	end
-	
+
 	local env = module.environment;
 	env.connected = false;
 
@@ -44,26 +44,26 @@
 		send = nil;
 		session.on_destroy = nil;
 	end
-	
+
 	-- Handle authentication attempts by component
 	local function handle_component_auth(event)
 		local session, stanza = event.origin, event.stanza;
-		
+
 		if session.type ~= "component_unauthed" then return; end
-	
+
 		if (not session.host) or #stanza.tags > 0 then
 			(session.log or log)("warn", "Invalid component handshake for host: %s", session.host);
 			session:close("not-authorized");
 			return true;
 		end
-		
+
 		local secret = module:get_option("component_secret");
 		if not secret then
 			(session.log or log)("warn", "Component attempted to identify as %s, but component_secret is not set", session.host);
 			session:close("not-authorized");
 			return true;
 		end
-		
+
 		local supplied_token = t_concat(stanza);
 		local calculated_token = sha1(session.streamid..secret, true);
 		if supplied_token:lower() ~= calculated_token:lower() then
@@ -71,13 +71,13 @@
 			session:close{ condition = "not-authorized", text = "Given token does not match calculated token" };
 			return true;
 		end
-		
+
 		if env.connected then
 			module:log("error", "Second component attempted to connect, denying connection");
 			session:close{ condition = "conflict", text = "Component already connected" };
 			return true;
 		end
-		
+
 		env.connected = true;
 		send = session.send;
 		session.on_destroy = on_destroy;
@@ -85,7 +85,7 @@
 		session.type = "component";
 		module:log("info", "External component successfully authenticated");
 		session.send(st.stanza("handshake"));
-	
+
 		return true;
 	end
 	module:hook("stanza/jabber:component:accept:handshake", handle_component_auth, -1);
@@ -116,7 +116,7 @@
 		end
 		return true;
 	end
-	
+
 	module:hook("iq/bare", handle_stanza, -1);
 	module:hook("message/bare", handle_stanza, -1);
 	module:hook("presence/bare", handle_stanza, -1);
@@ -177,9 +177,7 @@
 	session.streamid = uuid_gen();
 	session.notopen = nil;
 	-- Return stream header
-	session.send("<?xml version='1.0'?>");
-	session.send(st.stanza("stream:stream", { xmlns=xmlns_component,
-			["xmlns:stream"]='http://etherx.jabber.org/streams', id=session.streamid, from=session.host }):top_tag());
+	session:open_stream();
 end
 
 function stream_callbacks.streamclosed(session)
@@ -275,14 +273,14 @@
 	if opt_keepalives then
 		conn:setoption("keepalive", opt_keepalives);
 	end
-	
+
 	session.log("info", "Incoming Jabber component connection");
-	
+
 	local stream = new_xmpp_stream(session, stream_callbacks);
 	session.stream = stream;
-	
+
 	session.notopen = true;
-	
+
 	function session.reset_stream()
 		session.notopen = true;
 		session.stream:reset();
@@ -294,7 +292,7 @@
 		module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
 		session:close("not-well-formed");
 	end
-	
+
 	session.dispatch_stanza = stream_callbacks.handlestanza;
 
 	sessions[conn] = session;
--- a/plugins/mod_compression.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_compression.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,6 +1,6 @@
 -- Prosody IM
 -- Copyright (C) 2009-2012 Tobias Markmann
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -26,7 +26,7 @@
 
 module:hook("stream-features", function(event)
 	local origin, features = event.origin, event.features;
-	if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
+	if not origin.compressed and origin.type == "c2s" then
 		-- FIXME only advertise compression support when TLS layer has no compression enabled
 		features:add_child(compression_stream_feature);
 	end
@@ -35,7 +35,7 @@
 module:hook("s2s-stream-features", function(event)
 	local origin, features = event.origin, event.features;
 	-- FIXME only advertise compression support when TLS layer has no compression enabled
-	if not origin.compressed and (origin.type == "c2s" or origin.type == "s2sin" or origin.type == "s2sout") then
+	if not origin.compressed and origin.type == "s2sin" then
 		features:add_child(compression_stream_feature);
 	end
 end);
@@ -43,13 +43,13 @@
 -- Hook to activate compression if remote server supports it.
 module:hook_stanza(xmlns_stream, "features",
 		function (session, stanza)
-			if not session.compressed and (session.type == "c2s" or session.type == "s2sin" or session.type == "s2sout") then
+			if not session.compressed and session.type == "s2sout" then
 				-- does remote server support compression?
-				local comp_st = stanza:child_with_name("compression");
+				local comp_st = stanza:get_child("compression", xmlns_compression_feature);
 				if comp_st then
 					-- do we support the mechanism
-					for a in comp_st:children() do
-						local algorithm = a[1]
+					for a in comp_st:childtags("method") do
+						local algorithm = a:get_text();
 						if algorithm == "zlib" then
 							session.sends2s(st.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib"))
 							session.log("debug", "Enabled compression using zlib.")
@@ -103,7 +103,7 @@
 			return;
 		end
 		return compressed;
-	end);	
+	end);
 end
 
 -- setup decompression for a stream
@@ -131,13 +131,13 @@
 		-- create deflate and inflate streams
 		local deflate_stream = get_deflate_stream(session);
 		if not deflate_stream then return true; end
-		
+
 		local inflate_stream = get_inflate_stream(session);
 		if not inflate_stream then return true; end
-		
+
 		-- setup compression for session.w
 		setup_compression(session, deflate_stream);
-			
+
 		-- setup decompression for session.data
 		setup_decompression(session, inflate_stream);
 		session:reset_stream();
@@ -164,29 +164,28 @@
 			session.log("debug", "Client tried to establish another compression layer.");
 			return true;
 		end
-		
+
 		-- checking if the compression method is supported
-		local method = stanza:child_with_name("method");
-		method = method and (method[1] or "");
+		local method = stanza:get_child_text("method");
 		if method == "zlib" then
 			session.log("debug", "zlib compression enabled.");
-			
+
 			-- create deflate and inflate streams
 			local deflate_stream = get_deflate_stream(session);
 			if not deflate_stream then return true; end
-			
+
 			local inflate_stream = get_inflate_stream(session);
 			if not inflate_stream then return true; end
-			
+
 			(session.sends2s or session.send)(st.stanza("compressed", {xmlns=xmlns_compression_protocol}));
 			session:reset_stream();
-			
+
 			-- setup compression for session.w
 			setup_compression(session, deflate_stream);
-				
+
 			-- setup decompression for session.data
 			setup_decompression(session, inflate_stream);
-			
+
 			session.compressed = true;
 		elseif method then
 			session.log("debug", "%s compression selected, but we don't support it.", tostring(method));
--- a/plugins/mod_dialback.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_dialback.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -13,20 +13,33 @@
 local st = require "util.stanza";
 local sha256_hash = require "util.hashes".sha256;
 local nameprep = require "util.encodings".stringprep.nameprep;
+local check_cert_status = module:depends"s2s".check_cert_status;
+local uuid_gen = require"util.uuid".generate;
 
 local xmlns_stream = "http://etherx.jabber.org/streams";
 
 local dialback_requests = setmetatable({}, { __mode = 'v' });
 
+local dialback_secret = module.host .. module:get_option_string("dialback_secret", uuid_gen());
+local dwd = module:get_option_boolean("dialback_without_dialback", false);
+
+function module.save()
+	return { dialback_secret = dialback_secret };
+end
+
+function module.restore(state)
+	dialback_secret = state.dialback_secret;
+end
+
 function generate_dialback(id, to, from)
-	return sha256_hash(id..to..from..hosts[from].dialback_secret, true);
+	return sha256_hash(id..to..dialback_secret, true);
 end
 
 function initiate_dialback(session)
 	-- generate dialback key
 	session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host);
 	session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key));
-	session.log("info", "sent dialback key on outgoing s2s stream");
+	session.log("debug", "sent dialback key on outgoing s2s stream");
 end
 
 function verify_dialback(id, to, from, key)
@@ -35,7 +48,7 @@
 
 module:hook("stanza/jabber:server:dialback:verify", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
 		-- We are being asked to verify the key, to ensure it was generated by us
 		origin.log("debug", "verifying that dialback key is ours...");
@@ -62,26 +75,36 @@
 
 module:hook("stanza/jabber:server:dialback:result", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then
 		-- he wants to be identified through dialback
 		-- We need to check the key with the Authoritative server
 		local attr = stanza.attr;
 		local to, from = nameprep(attr.to), nameprep(attr.from);
-		
+
 		if not hosts[to] then
 			-- Not a host that we serve
-			origin.log("info", "%s tried to connect to %s, which we don't serve", from, to);
+			origin.log("warn", "%s tried to connect to %s, which we don't serve", from, to);
 			origin:close("host-unknown");
 			return true;
 		elseif not from then
 			origin:close("improper-addressing");
 		end
-		
+
+		if dwd and origin.secure then
+			if check_cert_status(origin, from) == false then
+				return
+			elseif origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then
+				origin.sends2s(st.stanza("db:result", { to = from, from = to, id = attr.id, type = "valid" }));
+				module:fire_event("s2s-authenticated", { session = origin, host = from });
+				return true;
+			end
+		end
+
 		origin.hosts[from] = { dialback_key = stanza[1] };
-		
+
 		dialback_requests[from.."/"..origin.streamid] = origin;
-		
+
 		-- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from'
 		-- on streams. We fill in the session's to/from here instead.
 		if not origin.from_host then
@@ -102,7 +125,7 @@
 
 module:hook("stanza/jabber:server:dialback:verify", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
 		local attr = stanza.attr;
 		local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")];
@@ -131,10 +154,10 @@
 
 module:hook("stanza/jabber:server:dialback:result", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	
+
 	if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then
 		-- Remote server is telling us whether we passed dialback
-		
+
 		local attr = stanza.attr;
 		if not hosts[attr.to] then
 			origin:close("host-unknown");
@@ -153,14 +176,6 @@
 	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");
-		initiate_dialback(origin);
-		return true;
-	end
-end, 100);
-
 module:hook_stanza(xmlns_stream, "features", function (origin, stanza)
 	if not origin.external_auth or origin.external_auth == "failed" then
 		module:log("debug", "Initiating dialback...");
--- a/plugins/mod_disco.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_disco.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -32,7 +32,9 @@
 	end
 end
 
-module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router
+if module:get_host_type() == "local" then
+	module:add_identity("server", "im", module:get_option_string("name", "Prosody")); -- FIXME should be in the non-existing mod_router
+end
 module:add_feature("http://jabber.org/protocol/disco#info");
 module:add_feature("http://jabber.org/protocol/disco#items");
 
@@ -97,7 +99,18 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then return; end -- TODO fire event?
+	if node and node ~= "" and node ~= "http://prosody.im#"..get_server_caps_hash() then
+		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
+		local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+		local ret = module:fire_event("host-disco-info-node", event);
+		if ret ~= nil then return ret; end
+		if event.exists then
+			origin.send(reply);
+		else
+			origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+		end
+		return true;
+	end
 	local reply_query = get_server_disco_info();
 	reply_query.node = node;
 	local reply = st.reply(stanza):add_child(reply_query);
@@ -108,9 +121,21 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" then return; end -- TODO fire event?
-
+	if node and node ~= "" then
+		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node});
+		local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+		local ret = module:fire_event("host-disco-items-node", event);
+		if ret ~= nil then return ret; end
+		if event.exists then
+			origin.send(reply);
+		else
+			origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+		end
+		return true;
+	end
 	local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
+	local ret = module:fire_event("host-disco-items", { origin = origin, stanza = stanza, reply = reply });
+	if ret ~= nil then return ret; end
 	for jid, name in pairs(get_children(module.host)) do
 		reply:tag("item", {jid = jid, name = name~=true and name or nil}):up();
 	end
@@ -133,12 +158,24 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" then return; end -- TODO fire event?
 	local username = jid_split(stanza.attr.to) or origin.username;
 	if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+		if node and node ~= "" then
+			local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node});
+			if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+			local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+			local ret = module:fire_event("account-disco-info-node", event);
+			if ret ~= nil then return ret; end
+			if event.exists then
+				origin.send(reply);
+			else
+				origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+			end
+			return true;
+		end
 		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'});
 		if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
-		module:fire_event("account-disco-info", { origin = origin, stanza = reply });
+		module:fire_event("account-disco-info", { origin = origin, reply = reply });
 		origin.send(reply);
 		return true;
 	end
@@ -147,12 +184,24 @@
 	local origin, stanza = event.origin, event.stanza;
 	if stanza.attr.type ~= "get" then return; end
 	local node = stanza.tags[1].attr.node;
-	if node and node ~= "" then return; end -- TODO fire event?
 	local username = jid_split(stanza.attr.to) or origin.username;
 	if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then
+		if node and node ~= "" then
+			local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items', node=node});
+			if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
+			local event = { origin = origin, stanza = stanza, reply = reply, node = node, exists = false};
+			local ret = module:fire_event("account-disco-items-node", event);
+			if ret ~= nil then return ret; end
+			if event.exists then
+				origin.send(reply);
+			else
+				origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Node does not exist"));
+			end
+			return true;
+		end
 		local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#items'});
 		if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account
-		module:fire_event("account-disco-items", { origin = origin, stanza = reply });
+		module:fire_event("account-disco-items", { origin = origin, stanza = stanza, reply = reply });
 		origin.send(reply);
 		return true;
 	end
--- a/plugins/mod_groups.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_groups.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -17,11 +17,13 @@
 
 local module_host = module:get_host();
 
-function inject_roster_contacts(username, host, roster)
+function inject_roster_contacts(event)
+	local username, host= event.username, event.host;
 	--module:log("debug", "Injecting group members to roster");
 	local bare_jid = username.."@"..host;
 	if not members[bare_jid] and not members[false] then return; end -- Not a member of any groups
-	
+
+	local roster = event.roster;
 	local function import_jids_to_roster(group_name)
 		for jid in pairs(groups[group_name]) do
 			-- Add them to roster
@@ -48,7 +50,7 @@
 			import_jids_to_roster(group_name);
 		end
 	end
-	
+
 	-- Import public groups
 	if members[false] then
 		for _, group_name in ipairs(members[false]) do
@@ -56,7 +58,7 @@
 			import_jids_to_roster(group_name);
 		end
 	end
-	
+
 	if roster[false] then
 		roster[false].version = true;
 	end
@@ -82,10 +84,10 @@
 function module.load()
 	groups_file = module:get_option_string("groups_file");
 	if not groups_file then return; end
-	
+
 	module:hook("roster-load", inject_roster_contacts);
 	datamanager.add_callback(remove_virtual_contacts);
-	
+
 	groups = { default = {} };
 	members = { };
 	local curr_group = "default";
--- a/plugins/mod_http.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_http.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2012 Matthew Wild
 -- Copyright (C) 2008-2012 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -45,6 +45,11 @@
 		:gsub("%$(%w+)", { host = host_module.host });
 end
 
+local function redir_handler(event)
+	event.response.headers.location = event.request.path.."/";
+	return 301;
+end
+
 local ports_by_scheme = { http = 80, https = 443, };
 
 -- Helper to deduce a module's external URL
@@ -79,6 +84,7 @@
 		local app_name = event.item.name;
 		local default_app_path = event.item.default_path or "/"..app_name;
 		local app_path = get_base_path(module, app_name, default_app_path);
+		module:log("debug", "Serving '%s' at %s", app_name, module:http_url(app_name, app_path));
 		if not app_name then
 			-- TODO: Link to docs
 			module:log("error", "HTTP app has no 'name', add one or use module:provides('http', app)");
@@ -99,6 +105,9 @@
 						local path = event.request.path:sub(base_path_len);
 						return _handler(event, path);
 					end;
+					module:hook_object_event(server, event_name:sub(1, -3), redir_handler, -1);
+				elseif event_name:sub(-1, -1) == "/" then
+					module:hook_object_event(server, event_name:sub(1, -2), redir_handler, -1);
 				end
 				if not app_handlers[event_name] then
 					app_handlers[event_name] = handler;
@@ -111,7 +120,7 @@
 			end
 		end
 	end
-	
+
 	local function http_app_removed(event)
 		local app_handlers = apps[event.item.name];
 		apps[event.item.name] = nil;
@@ -119,7 +128,7 @@
 			module:unhook_object_event(server, event, handler);
 		end
 	end
-	
+
 	module:handle_items("http-provider", http_app_added, http_app_removed);
 
 	server.add_host(host);
@@ -142,7 +151,13 @@
 	listener = server.listener;
 	default_port = 5281;
 	encryption = "ssl";
-	ssl_config = { verify = "none" };
+	ssl_config = {
+		verify = {
+			peer = false,
+			client_once = false,
+			"none",
+		}
+	};
 	multiplex = {
 		pattern = "^[A-Z]";
 	};
--- a/plugins/mod_http_errors.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_http_errors.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -53,7 +53,7 @@
 
 local function tohtml(plain)
 	return (plain:gsub("[<>&'\"\n]", entities));
-	
+
 end
 
 local function get_page(code, extra)
--- a/plugins/mod_http_files.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_http_files.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/plugins/mod_iq.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_iq.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/plugins/mod_lastactivity.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_lastactivity.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -19,8 +19,7 @@
 	local stanza = event.stanza;
 	if not(stanza.attr.to) and stanza.attr.type == "unavailable" then
 		local t = os.time();
-		local s = stanza:child_with_name("status");
-		s = s and #s.tags == 0 and s[1] or "";
+		local s = stanza:get_child_text("status");
 		map[event.origin.username] = {s = s, t = t};
 	end
 end, 10);
--- a/plugins/mod_legacyauth.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_legacyauth.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -11,8 +11,8 @@
 local st = require "util.stanza";
 local t_concat = table.concat;
 
-local secure_auth_only = module:get_option("c2s_require_encryption")
-	or module:get_option("require_encryption")
+local secure_auth_only = module:get_option("c2s_require_encryption",
+	module:get_option("require_encryption"))
 	or not(module:get_option("allow_unencrypted_plain_auth"));
 
 local sessionmanager = require "core.sessionmanager";
@@ -43,10 +43,11 @@
 		session.send(st.error_reply(stanza, "modify", "not-acceptable", "Encryption (SSL or TLS) is required to connect to this server"));
 		return true;
 	end
-	
-	local username = stanza.tags[1]:child_with_name("username");
-	local password = stanza.tags[1]:child_with_name("password");
-	local resource = stanza.tags[1]:child_with_name("resource");
+
+	local query = stanza.tags[1];
+	local username = query:get_child("username");
+	local password = query:get_child("password");
+	local resource = query:get_child("resource");
 	if not (username and password and resource) then
 		local reply = st.reply(stanza);
 		session.send(reply:query("jabber:iq:auth")
--- a/plugins/mod_message.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_message.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -17,7 +17,7 @@
 
 local function process_to_bare(bare, origin, stanza)
 	local user = bare_sessions[bare];
-	
+
 	local t = stanza.attr.type;
 	if t == "error" then
 		-- discard
@@ -66,7 +66,7 @@
 module:hook("message/full", function(data)
 	-- message to full JID recieved
 	local origin, stanza = data.origin, data.stanza;
-	
+
 	local session = full_sessions[stanza.attr.to];
 	if session and session.send(stanza) then
 		return true;
--- a/plugins/mod_motd.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_motd.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -2,7 +2,7 @@
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
 -- Copyright (C) 2010 Jeff Mitchell
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/plugins/mod_offline.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_offline.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- Prosody IM
 -- Copyright (C) 2008-2009 Matthew Wild
 -- Copyright (C) 2008-2009 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -24,11 +24,11 @@
 	else
 		node, host = origin.username, origin.host;
 	end
-	
+
 	stanza.attr.stamp, stanza.attr.stamp_legacy = datetime.datetime(), datetime.legacy();
 	local result = datamanager.list_append(node, host, "offline", st.preserialize(stanza));
 	stanza.attr.stamp, stanza.attr.stamp_legacy = nil, nil;
-	
+
 	return result;
 end);
 
--- a/plugins/mod_pep.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_pep.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -41,7 +41,8 @@
 	return is_contact_subscribed(username, host, recipient_bare);
 end
 
-local function publish(session, node, id, item)
+module:hook("pep-publish-item", function (event)
+	local session, node, id, item = event.session, event.node, event.id, event.item;
 	item.attr.xmlns = nil;
 	local disable = #item.tags ~= 1 or #item.tags[1] == 0;
 	if #item.tags == 0 then item.name = "retract"; end
@@ -72,7 +73,8 @@
 			core_post_stanza(session, stanza);
 		end
 	end
-end
+end);
+
 local function publish_all(user, recipient, session)
 	local d = data[user];
 	local notify = recipients[user] and recipients[user][recipient];
@@ -172,7 +174,9 @@
 				local id = payload.attr.id or "1";
 				payload.attr.id = id;
 				session.send(st.reply(stanza));
-				publish(session, node, id, st.clone(payload));
+				module:fire_event("pep-publish-item", {
+					node = node, actor = session.jid, id = id, session = session, item = st.clone(payload);
+				});
 				return true;
 			end
 		end
@@ -263,19 +267,19 @@
 end);
 
 module:hook("account-disco-info", function(event)
-	local stanza = event.stanza;
-	stanza:tag('identity', {category='pubsub', type='pep'}):up();
-	stanza:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
+	local reply = event.reply;
+	reply:tag('identity', {category='pubsub', type='pep'}):up();
+	reply:tag('feature', {var='http://jabber.org/protocol/pubsub#publish'}):up();
 end);
 
 module:hook("account-disco-items", function(event)
-	local stanza = event.stanza;
-	local bare = stanza.attr.to;
+	local reply = event.reply;
+	local bare = reply.attr.to;
 	local user_data = data[bare];
 
 	if user_data then
 		for node, _ in pairs(user_data) do
-			stanza:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes
+			reply:tag('item', {jid=bare, node=node}):up(); -- TODO we need to handle queries to these nodes
 		end
 	end
 end);
--- a/plugins/mod_ping.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_ping.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -11,14 +11,11 @@
 module:add_feature("urn:xmpp:ping");
 
 local function ping_handler(event)
-	if event.stanza.attr.type == "get" then
-		event.origin.send(st.reply(event.stanza));
-		return true;
-	end
+	return event.origin.send(st.reply(event.stanza));
 end
 
-module:hook("iq/bare/urn:xmpp:ping:ping", ping_handler);
-module:hook("iq/host/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq-get/bare/urn:xmpp:ping:ping", ping_handler);
+module:hook("iq-get/host/urn:xmpp:ping:ping", ping_handler);
 
 -- Ad-hoc command
 
--- a/plugins/mod_posix.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_posix.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -128,15 +128,7 @@
 end
 require "core.loggingmanager".register_sink_type("syslog", syslog_sink_maker);
 
-local daemonize = module:get_option("daemonize");
-if daemonize == nil then
-	local no_daemonize = module:get_option("no_daemonize"); --COMPAT w/ 0.5
-	daemonize = not no_daemonize;
-	if no_daemonize ~= nil then
-		module:log("warn", "The 'no_daemonize' option is now replaced by 'daemonize'");
-		module:log("warn", "Update your config from 'no_daemonize = %s' to 'daemonize = %s'", tostring(no_daemonize), tostring(daemonize));
-	end
-end
+local daemonize = module:get_option("daemonize", prosody.installed);
 
 local function remove_log_sinks()
 	local lm = require "core.loggingmanager";
@@ -183,7 +175,7 @@
 		prosody.reload_config();
 		prosody.reopen_logfiles();
 	end);
-	
+
 	signal.signal("SIGINT", function ()
 		module:log("info", "Received SIGINT");
 		prosody.unlock_globals();
--- a/plugins/mod_presence.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_presence.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -55,14 +55,14 @@
 
 function handle_normal_presence(origin, stanza)
 	if ignore_presence_priority then
-		local priority = stanza:child_with_name("priority");
+		local priority = stanza:get_child("priority");
 		if priority and priority[1] ~= "0" then
 			for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
 			for i=#priority,1,-1 do priority[i] = nil; end
 			priority[1] = "0";
 		end
 	end
-	local priority = stanza:child_with_name("priority");
+	local priority = stanza:get_child("priority");
 	if priority and #priority > 0 then
 		priority = t_concat(priority);
 		if s_find(priority, "^[+-]?[0-9]+$") then
@@ -90,6 +90,7 @@
 		end
 	end
 	if stanza.attr.type == nil and not origin.presence then -- initial presence
+		module:fire_event("presence/initial", { origin = origin, stanza = stanza } );
 		origin.presence = stanza; -- FIXME repeated later
 		local probe = st.presence({from = origin.full_jid, type = "probe"});
 		for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
@@ -227,7 +228,7 @@
 	local st_from, st_to = stanza.attr.from, stanza.attr.to;
 	stanza.attr.from, stanza.attr.to = from_bare, to_bare;
 	log("debug", "inbound presence %s from %s for %s", stanza.attr.type, from_bare, to_bare);
-	
+
 	if stanza.attr.type == "probe" then
 		local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
 		if result then
@@ -312,7 +313,7 @@
 		if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
 			return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
 		end
-	
+
 		local user = bare_sessions[to];
 		if user then
 			for _, session in pairs(user.sessions) do
@@ -347,7 +348,7 @@
 module:hook("presence/host", function(data)
 	-- inbound presence to the host
 	local stanza = data.stanza;
-	
+
 	local from_bare = jid_bare(stanza.attr.from);
 	local t = stanza.attr.type;
 	if t == "probe" then
--- a/plugins/mod_privacy.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_privacy.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -2,447 +2,12 @@
 -- Copyright (C) 2009-2010 Matthew Wild
 -- Copyright (C) 2009-2010 Waqas Hussain
 -- Copyright (C) 2009 Thilo Cestonaro
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
-module:add_feature("jabber:iq:privacy");
 
-local st = require "util.stanza";
-local bare_sessions, full_sessions = prosody.bare_sessions, prosody.full_sessions;
-local util_Jid = require "util.jid";
-local jid_bare = util_Jid.bare;
-local jid_split, jid_join = util_Jid.split, util_Jid.join;
-local load_roster = require "core.rostermanager".load_roster;
-local to_number = tonumber;
-
-local privacy_storage = module:open_store();
-
-function isListUsed(origin, name, privacy_lists)
-	local user = bare_sessions[origin.username.."@"..origin.host];
-	if user then
-		for resource, session in pairs(user.sessions) do
-			if resource ~= origin.resource then
-				if session.activePrivacyList == name then
-					return true;
-				elseif session.activePrivacyList == nil and privacy_lists.default == name then
-					return true;
-				end
-			end
-		end
-	end
-end
-
-function isAnotherSessionUsingDefaultList(origin)
-	local user = bare_sessions[origin.username.."@"..origin.host];
-	if user then
-		for resource, session in pairs(user.sessions) do
-			if resource ~= origin.resource and session.activePrivacyList == nil then
-				return true;
-			end
-		end
-	end
-end
-
-function declineList(privacy_lists, origin, stanza, which)
-	if which == "default" then
-		if isAnotherSessionUsingDefaultList(origin) then
-			return { "cancel", "conflict", "Another session is online and using the default list."};
-		end
-		privacy_lists.default = nil;
-		origin.send(st.reply(stanza));
-	elseif which == "active" then
-		origin.activePrivacyList = nil;
-		origin.send(st.reply(stanza));
-	else
-		return {"modify", "bad-request", "Neither default nor active list specifed to decline."};
-	end
-	return true;
-end
-
-function activateList(privacy_lists, origin, stanza, which, name)
-	local list = privacy_lists.lists[name];
-
-	if which == "default" and list then
-		if isAnotherSessionUsingDefaultList(origin) then
-			return {"cancel", "conflict", "Another session is online and using the default list."};
-		end
-		privacy_lists.default = name;
-		origin.send(st.reply(stanza));
-	elseif which == "active" and list then
-		origin.activePrivacyList = name;
-		origin.send(st.reply(stanza));
-	elseif not list then
-		return {"cancel", "item-not-found", "No such list: "..name};
-	else
-		return {"modify", "bad-request", "No list chosen to be active or default."};
-	end
-	return true;
-end
-
-function deleteList(privacy_lists, origin, stanza, name)
-	local list = privacy_lists.lists[name];
-
-	if list then
-		if isListUsed(origin, name, privacy_lists) then
-			return {"cancel", "conflict", "Another session is online and using the list which should be deleted."};
-		end
-		if privacy_lists.default == name then
-			privacy_lists.default = nil;
-		end
-		if origin.activePrivacyList == name then
-			origin.activePrivacyList = nil;
-		end
-		privacy_lists.lists[name] = nil;
-		origin.send(st.reply(stanza));
-		return true;
-	end
-	return {"modify", "bad-request", "Not existing list specifed to be deleted."};
-end
-
-function createOrReplaceList (privacy_lists, origin, stanza, name, entries)
-	local bare_jid = origin.username.."@"..origin.host;
-	
-	if privacy_lists.lists == nil then
-		privacy_lists.lists = {};
-	end
-
-	local list = {};
-	privacy_lists.lists[name] = list;
-
-	local orderCheck = {};
-	list.name = name;
-	list.items = {};
-
-	for _,item in ipairs(entries) do
-		if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then
-			return {"modify", "bad-request", "Order attribute not valid."};
-		end
-		
-		if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then
-			return {"modify", "bad-request", "Type attribute not valid."};
-		end
-		
-		local tmp = {};
-		orderCheck[item.attr.order] = true;
-		
-		tmp["type"] = item.attr.type;
-		tmp["value"] = item.attr.value;
-		tmp["action"] = item.attr.action;
-		tmp["order"] = to_number(item.attr.order);
-		tmp["presence-in"] = false;
-		tmp["presence-out"] = false;
-		tmp["message"] = false;
-		tmp["iq"] = false;
-		
-		if #item.tags > 0 then
-			for _,tag in ipairs(item.tags) do
-				tmp[tag.name] = true;
-			end
-		end
-		
-		if tmp.type == "subscription" then
-			if	tmp.value ~= "both" and
-				tmp.value ~= "to" and
-				tmp.value ~= "from" and
-				tmp.value ~= "none" then
-				return {"cancel", "bad-request", "Subscription value must be both, to, from or none."};
-			end
-		end
-		
-		if tmp.action ~= "deny" and tmp.action ~= "allow" then
-			return {"cancel", "bad-request", "Action must be either deny or allow."};
-		end
-		list.items[#list.items + 1] = tmp;
-	end
-	
-	table.sort(list.items, function(a, b) return a.order < b.order; end);
-
-	origin.send(st.reply(stanza));
-	if bare_sessions[bare_jid] ~= nil then
-		local iq = st.iq ( { type = "set", id="push1" } );
-		iq:tag ("query", { xmlns = "jabber:iq:privacy" } );
-		iq:tag ("list", { name = list.name } ):up();
-		iq:up();
-		for resource, session in pairs(bare_sessions[bare_jid].sessions) do
-			iq.attr.to = bare_jid.."/"..resource
-			session.send(iq);
-		end
-	else
-		return {"cancel", "bad-request", "internal error."};
-	end
-	return true;
-end
-
-function getList(privacy_lists, origin, stanza, name)
-	local reply = st.reply(stanza);
-	reply:tag("query", {xmlns="jabber:iq:privacy"});
-
-	if name == nil then
-		if privacy_lists.lists then
-			if origin.activePrivacyList then
-				reply:tag("active", {name=origin.activePrivacyList}):up();
-			end
-			if privacy_lists.default then
-				reply:tag("default", {name=privacy_lists.default}):up();
-			end
-			for name,list in pairs(privacy_lists.lists) do
-				reply:tag("list", {name=name}):up();
-			end
-		end
-	else
-		local list = privacy_lists.lists[name];
-		if list then
-			reply = reply:tag("list", {name=list.name});
-			for _,item in ipairs(list.items) do
-				reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order});
-				if item["message"] then reply:tag("message"):up(); end
-				if item["iq"] then reply:tag("iq"):up(); end
-				if item["presence-in"] then reply:tag("presence-in"):up(); end
-				if item["presence-out"] then reply:tag("presence-out"):up(); end
-				reply:up();
-			end
-		else
-			return {"cancel", "item-not-found", "Unknown list specified."};
-		end
-	end
-	
-	origin.send(reply);
-	return true;
-end
-
-module:hook("iq/bare/jabber:iq:privacy:query", function(data)
-	local origin, stanza = data.origin, data.stanza;
-	
-	if stanza.attr.to == nil then -- only service requests to own bare JID
-		local query = stanza.tags[1]; -- the query element
-		local valid = false;
-		local privacy_lists = privacy_storage:get(origin.username) or { lists = {} };
-
-		if privacy_lists.lists[1] then -- Code to migrate from old privacy lists format, remove in 0.8
-			module:log("info", "Upgrading format of stored privacy lists for %s@%s", origin.username, origin.host);
-			local lists = privacy_lists.lists;
-			for idx, list in ipairs(lists) do
-				lists[list.name] = list;
-				lists[idx] = nil;
-			end
-		end
-
-		if stanza.attr.type == "set" then
-			if #query.tags == 1 then --  the <query/> element MUST NOT include more than one child element
-				for _,tag in ipairs(query.tags) do
-					if tag.name == "active" or tag.name == "default" then
-						if tag.attr.name == nil then -- Client declines the use of active / default list
-							valid = declineList(privacy_lists, origin, stanza, tag.name);
-						else -- Client requests change of active / default list
-							valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name);
-						end
-					elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list
-						if #tag.tags == 0 then -- Client removes a privacy list
-							valid = deleteList(privacy_lists, origin, stanza, tag.attr.name);
-						else -- Client edits a privacy list
-							valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags);
-						end
-					end
-				end
-			end
-		elseif stanza.attr.type == "get" then
-			local name = nil;
-			local listsToRetrieve = 0;
-			if #query.tags >= 1 then
-				for _,tag in ipairs(query.tags) do
-					if tag.name == "list" then -- Client requests a privacy list from server
-						name = tag.attr.name;
-						listsToRetrieve = listsToRetrieve + 1;
-					end
-				end
-			end
-			if listsToRetrieve == 0 or listsToRetrieve == 1 then
-				valid = getList(privacy_lists, origin, stanza, name);
-			end
-		end
-
-		if valid ~= true then
-			valid = valid or { "cancel", "bad-request", "Couldn't understand request" };
-			if valid[1] == nil then
-				valid[1] = "cancel";
-			end
-			if valid[2] == nil then
-				valid[2] = "bad-request";
-			end
-			origin.send(st.error_reply(stanza, valid[1], valid[2], valid[3]));
-		else
-			privacy_storage:set(origin.username, privacy_lists);
-		end
-		return true;
-	end
-end);
-
-function checkIfNeedToBeBlocked(e, session)
-	local origin, stanza = e.origin, e.stanza;
-	local privacy_lists = privacy_storage:get(session.username) or {};
-	local bare_jid = session.username.."@"..session.host;
-	local to = stanza.attr.to or bare_jid;
-	local from = stanza.attr.from;
-	
-	local is_to_user = bare_jid == jid_bare(to);
-	local is_from_user = bare_jid == jid_bare(from);
-	
-	--module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
-	
-	if privacy_lists.lists == nil or
-		not (session.activePrivacyList or privacy_lists.default)
-	then
-		return; -- Nothing to block, default is Allow all
-	end
-	if is_from_user and is_to_user then
-		--module:log("debug", "Not blocking communications between user's resources");
-		return; -- from one of a user's resource to another => HANDS OFF!
-	end
-	
-	local listname = session.activePrivacyList;
-	if listname == nil then
-		listname = privacy_lists.default; -- no active list selected, use default list
-	end
-	local list = privacy_lists.lists[listname];
-	if not list then -- should never happen
-		module:log("warn", "given privacy list not found. name: %s for user %s", listname, bare_jid);
-		return;
-	end
-	for _,item in ipairs(list.items) do
-		local apply = false;
-		local block = false;
-		if (
-			(stanza.name == "message" and item.message) or
-			(stanza.name == "iq" and item.iq) or
-			(stanza.name == "presence" and is_to_user and item["presence-in"]) or
-			(stanza.name == "presence" and is_from_user and item["presence-out"]) or
-			(item.message == false and item.iq == false and item["presence-in"] == false and item["presence-out"] == false)
-		) then
-			apply = true;
-		end
-		if apply then
-			local evilJid = {};
-			apply = false;
-			if is_to_user then
-				--module:log("debug", "evil jid is (from): %s", from);
-				evilJid.node, evilJid.host, evilJid.resource = jid_split(from);
-			else
-				--module:log("debug", "evil jid is (to): %s", to);
-				evilJid.node, evilJid.host, evilJid.resource = jid_split(to);
-			end
-			if	item.type == "jid" and
-				(evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or
-				(evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or
-				(evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or
-				(evilJid.host and item.value == evilJid.host) then
-				apply = true;
-				block = (item.action == "deny");
-			elseif item.type == "group" then
-				local roster = load_roster(session.username, session.host);
-				local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
-				if roster_entry then
-					local groups = roster_entry.groups;
-					for group in pairs(groups) do
-						if group == item.value then
-							apply = true;
-							block = (item.action == "deny");
-							break;
-						end
-					end
-				end
-			elseif item.type == "subscription" then -- we need a valid bare evil jid
-				local roster = load_roster(session.username, session.host);
-				local roster_entry = roster[jid_join(evilJid.node, evilJid.host)];
-				if (not(roster_entry) and item.value == "none")
-				   or (roster_entry and roster_entry.subscription == item.value) then
-					apply = true;
-					block = (item.action == "deny");
-				end
-			elseif item.type == nil then
-				apply = true;
-				block = (item.action == "deny");
-			end
-		end
-		if apply then
-			if block then
-				-- drop and not bounce groupchat messages, otherwise users will get kicked
-				if stanza.attr.type == "groupchat" then
-					return true;
-				end
-				module:log("debug", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(to), tostring(from));
-				if stanza.name == "message" then
-					origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
-				elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then
-					origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
-				end
-				return true; -- stanza blocked !
-			else
-				--module:log("debug", "stanza explicitly allowed!")
-				return;
-			end
-		end
-	end
-end
-
-function preCheckIncoming(e)
-	local session;
-	if e.stanza.attr.to ~= nil then
-		local node, host, resource = jid_split(e.stanza.attr.to);
-		if node == nil or host == nil then
-			return;
-		end
-		if resource == nil then
-			local prio = 0;
-			if bare_sessions[node.."@"..host] ~= nil then
-				for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do
-					if session_.priority ~= nil and session_.priority > prio then
-						session = session_;
-						prio = session_.priority;
-					end
-				end
-			end
-		else
-			session = full_sessions[node.."@"..host.."/"..resource];
-		end
-		if session ~= nil then
-			return checkIfNeedToBeBlocked(e, session);
-		else
-			--module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource));
-		end
-	end
-end
-
-function preCheckOutgoing(e)
-	local session = e.origin;
-	if e.stanza.attr.from == nil then
-		e.stanza.attr.from = session.username .. "@" .. session.host;
-		if session.resource ~= nil then
-		 	e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource;
-		end
-	end
-	if session.username then -- FIXME do properly
-		return checkIfNeedToBeBlocked(e, session);
-	end
-end
-
-module:hook("pre-message/full", preCheckOutgoing, 500);
-module:hook("pre-message/bare", preCheckOutgoing, 500);
-module:hook("pre-message/host", preCheckOutgoing, 500);
-module:hook("pre-iq/full", preCheckOutgoing, 500);
-module:hook("pre-iq/bare", preCheckOutgoing, 500);
-module:hook("pre-iq/host", preCheckOutgoing, 500);
-module:hook("pre-presence/full", preCheckOutgoing, 500);
-module:hook("pre-presence/bare", preCheckOutgoing, 500);
-module:hook("pre-presence/host", preCheckOutgoing, 500);
-
-module:hook("message/full", preCheckIncoming, 500);
-module:hook("message/bare", preCheckIncoming, 500);
-module:hook("message/host", preCheckIncoming, 500);
-module:hook("iq/full", preCheckIncoming, 500);
-module:hook("iq/bare", preCheckIncoming, 500);
-module:hook("iq/host", preCheckIncoming, 500);
-module:hook("presence/full", preCheckIncoming, 500);
-module:hook("presence/bare", preCheckIncoming, 500);
-module:hook("presence/host", preCheckIncoming, 500);
+-- COMPAT w/ pre 0.10
+module:log("error", "The mod_privacy plugin has been replaced by mod_blocklist. Please update your config. For more information see https://prosody.im/doc/modules/mod_privacy");
+module:depends("blocklist");
--- a/plugins/mod_private.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_private.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -15,38 +15,34 @@
 
 module:hook("iq/self/jabber:iq:private:query", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	local type = stanza.attr.type;
 	local query = stanza.tags[1];
-	if #query.tags == 1 then
-		local tag = query.tags[1];
-		local key = tag.name..":"..tag.attr.xmlns;
-		local data, err = private_storage:get(origin.username);
-		if err then
-			origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
-			return true;
+	if #query.tags ~= 1 then
+		return origin.send(st.error_reply(stanza, "modify", "bad-format"));
+	end
+	local tag = query.tags[1];
+	local key = tag.name..":"..tag.attr.xmlns;
+	local data, err = private_storage:get(origin.username);
+	if err then
+		return origin.send(st.error_reply(stanza, "wait", "internal-server-error", err));
+	end
+	if stanza.attr.type == "get" then
+		if data and data[key] then
+			return origin.send(st.reply(stanza):query("jabber:iq:private"):add_child(st.deserialize(data[key])));
+		else
+			return origin.send(st.reply(stanza):add_child(query));
 		end
-		if stanza.attr.type == "get" then
-			if data and data[key] then
-				origin.send(st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"}):add_child(st.deserialize(data[key])));
-			else
-				origin.send(st.reply(stanza):add_child(stanza.tags[1]));
-			end
-		else -- set
-			if not data then data = {}; end;
-			if #tag == 0 then
-				data[key] = nil;
-			else
-				data[key] = st.preserialize(tag);
-			end
-			-- TODO delete datastore if empty
-			if private_storage:set(origin.username, data) then
-				origin.send(st.reply(stanza));
-			else
-				origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
-			end
+	else -- type == set
+		if not data then data = {}; end;
+		if #tag == 0 then
+			data[key] = nil;
+		else
+			data[key] = st.preserialize(tag);
 		end
-	else
-		origin.send(st.error_reply(stanza, "modify", "bad-format"));
+		-- TODO delete datastore if empty
+		local ok, err = private_storage:set(origin.username, data);
+		if not ok then
+			return origin.send(st.error_reply(stanza, "wait", "internal-server-error", err));
+		end
+		return origin.send(st.reply(stanza));
 	end
-	return true;
 end);
--- a/plugins/mod_proxy65.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_proxy65.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -2,7 +2,7 @@
 -- Copyright (C) 2008-2011 Matthew Wild
 -- Copyright (C) 2008-2011 Waqas Hussain
 -- Copyright (C) 2009 Thilo Cestonaro
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
@@ -30,7 +30,7 @@
 		(conn == initiator and target or initiator):write(data);
 		return;
 	end -- FIXME server.link should be doing this?
-	
+
 	if not session.greeting_done then
 		local nmethods = data:byte(2) or 0;
 		if data:byte(1) == 0x05 and nmethods > 0 and #data == 2 + nmethods then -- check if we have all the data
@@ -90,7 +90,7 @@
 
 function module.add_host(module)
 	local host, name = module:get_host(), module:get_option_string("name", "SOCKS5 Bytestreams Service");
-	
+
 	local proxy_address = module:get_option("proxy65_address", host);
 	local proxy_port = next(portmanager.get_active_services():search("proxy65", nil)[1] or {});
 	local proxy_acl = module:get_option("proxy65_acl");
@@ -101,30 +101,13 @@
 		module:log("warn", "proxy65_port is deprecated, please put proxy65_ports = { %d } into the global section instead", legacy_config);
 	end
 
+	module:depends("disco");
 	module:add_identity("proxy", "bytestreams", name);
 	module:add_feature("http://jabber.org/protocol/bytestreams");
-	
-	module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function(event)
-		local origin, stanza = event.origin, event.stanza;
-		if not stanza.tags[1].attr.node then
-			origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#info")
-				:tag("identity", {category='proxy', type='bytestreams', name=name}):up()
-				:tag("feature", {var="http://jabber.org/protocol/bytestreams"}) );
-			return true;
-		end
-	end, -1);
-	
-	module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function(event)
-		local origin, stanza = event.origin, event.stanza;
-		if not stanza.tags[1].attr.node then
-			origin.send(st.reply(stanza):query("http://jabber.org/protocol/disco#items"));
-			return true;
-		end
-	end, -1);
-	
+
 	module:hook("iq-get/host/http://jabber.org/protocol/bytestreams:query", function(event)
 		local origin, stanza = event.origin, event.stanza;
-		
+
 		-- check ACL
 		while proxy_acl and #proxy_acl > 0 do -- using 'while' instead of 'if' so we can break out of it
 			local jid = stanza.attr.from;
@@ -137,22 +120,22 @@
 			origin.send(st.error_reply(stanza, "auth", "forbidden"));
 			return true;
 		end
-	
+
 		local sid = stanza.tags[1].attr.sid;
 		origin.send(st.reply(stanza):tag("query", {xmlns="http://jabber.org/protocol/bytestreams", sid=sid})
 			:tag("streamhost", {jid=host, host=proxy_address, port=proxy_port}));
 		return true;
 	end);
-	
+
 	module:hook("iq-set/host/http://jabber.org/protocol/bytestreams:query", function(event)
 		local origin, stanza = event.origin, event.stanza;
-	
+
 		local query = stanza.tags[1];
 		local sid = query.attr.sid;
 		local from = stanza.attr.from;
 		local to = query:get_child_text("activate");
 		local prepped_to = jid_prep(to);
-	
+
 		local info = "sid: "..tostring(sid)..", initiator: "..tostring(from)..", target: "..tostring(prepped_to or to);
 		if prepped_to and sid then
 			local sha = sha1(sid .. from .. prepped_to, true);
--- a/plugins/mod_pubsub.lua	Tue Feb 24 15:19:20 2015 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,463 +0,0 @@
-local pubsub = require "util.pubsub";
-local st = require "util.stanza";
-local jid_bare = require "util.jid".bare;
-local uuid_generate = require "util.uuid".generate;
-local usermanager = require "core.usermanager";
-
-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 xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
-
-local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
-local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
-local pubsub_disco_name = module:get_option("name");
-if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end
-
-local service;
-
-local handlers = {};
-
-function handle_pubsub_iq(event)
-	local origin, stanza = event.origin, event.stanza;
-	local pubsub = stanza.tags[1];
-	local action = pubsub.tags[1];
-	if not action then
-		return origin.send(st.error_reply(stanza, "cancel", "bad-request"));
-	end
-	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" };
-	["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
-	["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
-	["item-not-found"] = { "cancel", "item-not-found" };
-	["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
-	["forbidden"] = { "auth", "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;
-	
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	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
-	local reply;
-	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_delete(origin, stanza, delete)
-	local node = delete.attr.node;
-
-	local reply, notifier;
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	local ok, ret = service:delete(node, stanza.attr.from);
-	if ok then
-		reply = st.reply(stanza);
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	return origin.send(reply);
-end
-
-function handlers.set_subscribe(origin, stanza, subscribe)
-	local node, jid = subscribe.attr.node, subscribe.attr.jid;
-	if not (node and jid) then
-		return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
-	end
-	--[[
-	local options_tag, options = stanza.tags[1]:get_child("options"), nil;
-	if options_tag then
-		options = options_form:data(options_tag.tags[1]);
-	end
-	--]]
-	local options_tag, options; -- FIXME
-	local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
-	local reply;
-	if ok then
-		reply = st.reply(stanza)
-			:tag("pubsub", { xmlns = xmlns_pubsub })
-				:tag("subscription", {
-					node = node,
-					jid = jid,
-					subscription = "subscribed"
-				}):up();
-		if options_tag then
-			reply:add_child(options_tag);
-		end
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	origin.send(reply);
-end
-
-function handlers.set_unsubscribe(origin, stanza, unsubscribe)
-	local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
-	if not (node and jid) then
-		return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "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;
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	local item = publish:get_child("item");
-	local id = (item and item.attr.id);
-	if not id then
-		id = uuid_generate();
-		if item then
-			item.attr.id = id;
-		end
-	end
-	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
-	if not (node and id) then
-		return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
-	end
-	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 handlers.set_purge(origin, stanza, purge)
-	local node, notify = purge.attr.node, purge.attr.notify;
-	notify = (notify == "1") or (notify == "true");
-	local reply;
-	if not node then
-		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
-	end
-	local ok, ret = service:purge(node, stanza.attr.from, notify);
-	if ok then
-		reply = st.reply(stanza);
-	else
-		reply = pubsub_error_reply(stanza, ret);
-	end
-	return origin.send(reply);
-end
-
-function simple_broadcast(kind, node, jids, item)
-	if item then
-		item = st.clone(item);
-		item.attr.xmlns = nil; -- Clear the pubsub namespace
-	end
-	local message = st.message({ from = module.host, type = "headline" })
-		:tag("event", { xmlns = xmlns_pubsub_event })
-			:tag(kind, { node = node })
-				:add_child(item);
-	for jid in pairs(jids) do
-		module:log("debug", "Sending notification to %s", jid);
-		message.attr.to = jid;
-		module:send(message);
-	end
-end
-
-module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
-module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
-
-local disco_info;
-
-local feature_map = {
-	create = { "create-nodes", "instant-nodes", "item-ids" };
-	retract = { "delete-items", "retract-items" };
-	purge = { "purge-nodes" };
-	publish = { "publish", autocreate_on_publish and "auto-create" };
-	delete = { "delete-nodes" };
-	get_items = { "retrieve-items" };
-	add_subscription = { "subscribe" };
-	get_subscriptions = { "retrieve-subscriptions" };
-};
-
-local function add_disco_features_from_service(disco, service)
-	for method, features in pairs(feature_map) do
-		if service[method] then
-			for _, feature in ipairs(features) do
-				if feature then
-					disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
-				end
-			end
-		end
-	end
-	for affiliation in pairs(service.config.capabilities) do
-		if affiliation ~= "none" and affiliation ~= "owner" then
-			disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up();
-		end
-	end
-end
-
-local function build_disco_info(service)
-	local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
-		:tag("identity", { category = "pubsub", type = "service", name = pubsub_disco_name }):up()
-		:tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
-	add_disco_features_from_service(disco_info, service);
-	return disco_info;
-end
-
-module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
-	local origin, stanza = event.origin, event.stanza;
-	local node = stanza.tags[1].attr.node;
-	if not node then
-		return origin.send(st.reply(stanza):add_child(disco_info));
-	else
-		local ok, ret = service:get_nodes(stanza.attr.from);
-		if ok and not ret[node] then
-			ok, ret = false, "item-not-found";
-		end
-		if not ok then
-			return origin.send(pubsub_error_reply(stanza, ret));
-		end
-		local reply = st.reply(stanza)
-			:tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node })
-				:tag("identity", { category = "pubsub", type = "leaf" });
-		return origin.send(reply);
-	end
-end);
-
-local function handle_disco_items_on_node(event)
-	local stanza, origin = event.stanza, event.origin;
-	local query = stanza.tags[1];
-	local node = query.attr.node;
-	local ok, ret = service:get_items(node, stanza.attr.from);
-	if not ok then
-		return origin.send(pubsub_error_reply(stanza, ret));
-	end
-	
-	local reply = st.reply(stanza)
-		:tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
-	
-	for id, item in pairs(ret) do
-		reply:tag("item", { jid = module.host, name = id }):up();
-	end
-	
-	return origin.send(reply);
-end
-
-
-module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
-	if event.stanza.tags[1].attr.node then
-		return handle_disco_items_on_node(event);
-	end
-	local ok, ret = service:get_nodes(event.stanza.attr.from);
-	if not ok then
-		event.origin.send(pubsub_error_reply(event.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;
-		};
-		publisher = {
-			create = false;
-			publish = true;
-			retract = true;
-			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;
-			delete = 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;
-}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_pubsub/mod_pubsub.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,233 @@
+local pubsub = require "util.pubsub";
+local st = require "util.stanza";
+local jid_bare = require "util.jid".bare;
+local usermanager = require "core.usermanager";
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
+local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
+
+local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
+local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
+local pubsub_disco_name = module:get_option("name");
+if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end
+local expose_publisher = module:get_option_boolean("expose_publisher", false)
+
+local service;
+
+local lib_pubsub = module:require "pubsub";
+local handlers = lib_pubsub.handlers;
+local pubsub_error_reply = lib_pubsub.pubsub_error_reply;
+
+module:depends("disco");
+module:add_identity("pubsub", "service", pubsub_disco_name);
+module:add_feature("http://jabber.org/protocol/pubsub");
+
+function handle_pubsub_iq(event)
+	local origin, stanza = event.origin, event.stanza;
+	local pubsub = stanza.tags[1];
+	local action = pubsub.tags[1];
+	if not action then
+		return origin.send(st.error_reply(stanza, "cancel", "bad-request"));
+	end
+	local handler = handlers[stanza.attr.type.."_"..action.name];
+	if handler then
+		handler(origin, stanza, action, service);
+		return true;
+	end
+end
+
+function simple_broadcast(kind, node, jids, item, actor)
+	if item then
+		item = st.clone(item);
+		item.attr.xmlns = nil; -- Clear the pubsub namespace
+		if expose_publisher and actor then
+			item.attr.publisher = actor
+		end
+	end
+	local message = st.message({ from = module.host, type = "headline" })
+		:tag("event", { xmlns = xmlns_pubsub_event })
+			:tag(kind, { node = node })
+				:add_child(item);
+	for jid in pairs(jids) do
+		module:log("debug", "Sending notification to %s", jid);
+		message.attr.to = jid;
+		module:send(message);
+	end
+end
+
+module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
+module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
+
+local feature_map = {
+	create = { "create-nodes", "instant-nodes", "item-ids" };
+	retract = { "delete-items", "retract-items" };
+	purge = { "purge-nodes" };
+	publish = { "publish", autocreate_on_publish and "auto-create" };
+	delete = { "delete-nodes" };
+	get_items = { "retrieve-items" };
+	add_subscription = { "subscribe" };
+	get_subscriptions = { "retrieve-subscriptions" };
+	set_configure = { "config-node" };
+	get_default = { "retrieve-default" };
+};
+
+local function add_disco_features_from_service(service)
+	for method, features in pairs(feature_map) do
+		if service[method] then
+			for _, feature in ipairs(features) do
+				if feature then
+					module:add_feature(xmlns_pubsub.."#"..feature);
+				end
+			end
+		end
+	end
+	for affiliation in pairs(service.config.capabilities) do
+		if affiliation ~= "none" and affiliation ~= "owner" then
+			module:add_feature(xmlns_pubsub.."#"..affiliation.."-affiliation");
+		end
+	end
+end
+
+module:hook("host-disco-info-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	local ok, ret = service:get_nodes(stanza.attr.from);
+	if not ok or not ret[node] then
+		return;
+	end
+	event.exists = true;
+	reply:tag("identity", { category = "pubsub", type = "leaf" });
+end);
+
+module:hook("host-disco-items-node", function (event)
+	local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node;
+	local ok, ret = service:get_items(node, stanza.attr.from);
+	if not ok then
+		return;
+	end
+
+	for _, id in ipairs(ret) do
+		reply:tag("item", { jid = module.host, name = id }):up();
+	end
+	event.exists = true;
+end);
+
+
+module:hook("host-disco-items", function (event)
+	local stanza, origin, reply = event.stanza, event.origin, event.reply;
+	local ok, ret = service:get_nodes(event.stanza.attr.from);
+	if not ok then
+		return;
+	end
+	for node, node_obj in pairs(ret) do
+		reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
+	end
+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;
+	add_disco_features_from_service(service);
+end
+
+function module.save()
+	return { service = service };
+end
+
+function module.restore(data)
+	set_service(data.service);
+end
+
+function module.load()
+	if module.reloading then return; 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;
+			};
+			publisher = {
+				create = false;
+				publish = true;
+				retract = true;
+				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;
+				delete = true;
+				get_nodes = true;
+				configure = 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;
+	}));
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_pubsub/pubsub.lib.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,290 @@
+local st = require "util.stanza";
+local uuid_generate = require "util.uuid".generate;
+local dataform = require"util.dataforms".new;
+
+local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
+local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
+local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
+
+local _M = {};
+
+local handlers = {};
+_M.handlers = handlers;
+
+local pubsub_errors = {
+	["conflict"] = { "cancel", "conflict" };
+	["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
+	["jid-required"] = { "modify", "bad-request", nil, "jid-required" };
+	["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" };
+	["item-not-found"] = { "cancel", "item-not-found" };
+	["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
+	["forbidden"] = { "auth", "forbidden" };
+	["not-allowed"] = { "cancel", "not-allowed" };
+};
+local 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
+_M.pubsub_error_reply = pubsub_error_reply;
+
+local node_config_form = require"util.dataforms".new {
+	{
+		type = "hidden";
+		name = "FORM_TYPE";
+		value = "http://jabber.org/protocol/pubsub#node_config";
+	};
+	{
+		type = "text-single";
+		name = "pubsub#max_items";
+		label = "Max # of items to persist";
+	};
+};
+
+function handlers.get_items(origin, stanza, items, service)
+	local node = items.attr.node;
+	local item = items:get_child("item");
+	local id = item and item.attr.id;
+
+	if not node then
+		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+	end
+	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 _, id in ipairs(results) do
+		data:add_child(results[id]);
+	end
+	local reply;
+	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, service)
+	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, service)
+	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_delete(origin, stanza, delete, service)
+	local node = delete.attr.node;
+
+	local reply, notifier;
+	if not node then
+		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+	end
+	local ok, ret = service:delete(node, stanza.attr.from);
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	return origin.send(reply);
+end
+
+function handlers.set_subscribe(origin, stanza, subscribe, service)
+	local node, jid = subscribe.attr.node, subscribe.attr.jid;
+	if not (node and jid) then
+		return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid"));
+	end
+	--[[
+	local options_tag, options = stanza.tags[1]:get_child("options"), nil;
+	if options_tag then
+		options = options_form:data(options_tag.tags[1]);
+	end
+	--]]
+	local options_tag, options; -- FIXME
+	local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options);
+	local reply;
+	if ok then
+		reply = st.reply(stanza)
+			:tag("pubsub", { xmlns = xmlns_pubsub })
+				:tag("subscription", {
+					node = node,
+					jid = jid,
+					subscription = "subscribed"
+				}):up();
+		if options_tag then
+			reply:add_child(options_tag);
+		end
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	origin.send(reply);
+end
+
+function handlers.set_unsubscribe(origin, stanza, unsubscribe, service)
+	local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
+	if not (node and jid) then
+		return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "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, service)
+	local node = publish.attr.node;
+	if not node then
+		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+	end
+	local item = publish:get_child("item");
+	local id = (item and item.attr.id);
+	if not id then
+		id = uuid_generate();
+		if item then
+			item.attr.id = id;
+		end
+	end
+	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, service)
+	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
+	if not (node and id) then
+		return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required"));
+	end
+	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 handlers.set_purge(origin, stanza, purge, service)
+	local node, notify = purge.attr.node, purge.attr.notify;
+	notify = (notify == "1") or (notify == "true");
+	local reply;
+	if not node then
+		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+	end
+	local ok, ret = service:purge(node, stanza.attr.from, notify);
+	if ok then
+		reply = st.reply(stanza);
+	else
+		reply = pubsub_error_reply(stanza, ret);
+	end
+	return origin.send(reply);
+end
+
+function handlers.get_configure(origin, stanza, config, service)
+	local node = config.attr.node;
+	if not node then
+		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+	end
+
+	if not service:may(node, stanza.attr.from, "configure") then
+		return origin.send(pubsub_error_reply(stanza, "forbidden"));
+	end
+
+	local node_obj = service.nodes[node];
+	if not node_obj then
+		return origin.send(pubsub_error_reply(stanza, "item-not-found"));
+	end
+
+	local reply = st.reply(stanza)
+		:tag("pubsub", { xmlns = xmlns_pubsub_owner })
+			:tag("configure", { node = node })
+				:add_child(node_config_form:form(node_obj.config));
+	return origin.send(reply);
+end
+
+function handlers.set_configure(origin, stanza, config, service)
+	local node = config.attr.node;
+	if not node then
+		return origin.send(pubsub_error_reply(stanza, "nodeid-required"));
+	end
+	if not service:may(node, stanza.attr.from, "configure") then
+		return origin.send(pubsub_error_reply(stanza, "forbidden"));
+	end
+	local new_config, err = node_config_form:data(config.tags[1]);
+	if not new_config then
+		return origin.send(st.error_reply(stanza, "modify", "bad-request", err));
+	end
+	local ok, err = service:set_node_config(node, stanza.attr.from, new_config);
+	if not ok then
+		return origin.send(pubsub_error_reply(stanza, err));
+	end
+	return origin.send(st.reply(stanza));
+end
+
+function handlers.get_default(origin, stanza, default, service)
+	local reply = st.reply(stanza)
+		:tag("pubsub", { xmlns = xmlns_pubsub_owner })
+			:tag("default")
+				:add_child(node_config_form:form(service.node_defaults));
+	return origin.send(reply);
+end
+
+return _M;
--- a/plugins/mod_register.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_register.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -72,7 +72,7 @@
 
 local register_stream_feature = st.stanza("register", {xmlns="http://jabber.org/features/iq-register"}):up();
 module:hook("stream-features", function(event)
-        local session, features = event.origin, event.features;
+	local session, features = event.origin, event.features;
 
 	-- Advertise registration to unauthorized clients only.
 	if not(allow_registration) or session.type ~= "c2s_unauthed" then
@@ -102,16 +102,16 @@
 				session.send(st.reply(stanza));
 				return old_session_close(session, ...);
 			end
-			
+
 			local ok, err = usermanager_delete_user(username, host);
-			
+
 			if not ok then
 				module:log("debug", "Removing user account %s@%s failed: %s", username, host, err);
 				session.close = old_session_close;
 				session.send(st.error_reply(stanza, "cancel", "service-unavailable", err));
 				return true;
 			end
-			
+
 			module:log("info", "User removed their account: %s@%s", username, host);
 			module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session });
 		else
@@ -170,13 +170,10 @@
 end
 
 local recent_ips = {};
-local min_seconds_between_registrations = module:get_option("min_seconds_between_registrations");
-local whitelist_only = module:get_option("whitelist_registration_only");
-local whitelisted_ips = module:get_option("registration_whitelist") or { "127.0.0.1" };
-local blacklisted_ips = module:get_option("registration_blacklist") or {};
-
-for _, ip in ipairs(whitelisted_ips) do whitelisted_ips[ip] = true; end
-for _, ip in ipairs(blacklisted_ips) do blacklisted_ips[ip] = true; end
+local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations");
+local whitelist_only = module:get_option_boolean("whitelist_registration_only");
+local whitelisted_ips = module:get_option_set("registration_whitelist", { "127.0.0.1" })._items;
+local blacklisted_ips = module:get_option_set("registration_blacklist", {})._items;
 
 module:hook("stanza/iq/jabber:iq:register:query", function(event)
 	local session, stanza = event.origin, event.stanza;
@@ -209,7 +206,7 @@
 						else
 							local ip = recent_ips[session.ip];
 							ip.count = ip.count + 1;
-							
+
 							if os_time() - ip.time < min_seconds_between_registrations then
 								ip.time = os_time();
 								session.send(st.error_reply(stanza, "wait", "not-acceptable"));
--- a/plugins/mod_roster.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_roster.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -36,10 +36,10 @@
 
 	if stanza.attr.type == "get" then
 		local roster = st.reply(stanza);
-		
+
 		local client_ver = tonumber(stanza.tags[1].attr.ver);
 		local server_ver = tonumber(session.roster[false].version or 1);
-		
+
 		if not (client_ver and server_ver) or client_ver ~= server_ver then
 			roster:query("jabber:iq:roster");
 			-- Client does not support versioning, or has stale roster
--- a/plugins/mod_s2s/mod_s2s.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_s2s/mod_s2s.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -15,7 +15,6 @@
 local tostring, type = tostring, type;
 local t_insert = table.insert;
 local xpcall, traceback = xpcall, debug.traceback;
-local NULL = {};
 
 local add_task = require "util.timer".add_task;
 local st = require "util.stanza";
@@ -26,7 +25,6 @@
 local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
 local s2s_destroy_session = require "core.s2smanager".destroy_session;
 local uuid_gen = require "util.uuid".generate;
-local cert_verify_identity = require "util.x509".verify_identity;
 local fire_global_event = prosody.events.fire_event;
 
 local s2sout = module:require("s2sout");
@@ -135,6 +133,12 @@
 	return true;
 end
 
+local function keepalive(event)
+	return event.session.sends2s(' ');
+end
+
+module:hook("s2s-read-timeout", keepalive, -1);
+
 function module.add_host(module)
 	if module:get_option_boolean("disallow_s2s", false) then
 		module:log("warn", "The 'disallow_s2s' config option is deprecated, please see http://prosody.im/doc/s2s#disabling");
@@ -143,15 +147,28 @@
 	module:hook("route/remote", route_to_existing_session, -1);
 	module:hook("route/remote", route_to_new_session, -10);
 	module:hook("s2s-authenticated", make_authenticated, -1);
+	module:hook("s2s-read-timeout", keepalive, -1);
+	module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
+		if session.type == "s2sout" then
+			-- Stream is authenticated and we are seem to be done with feature negotiation,
+			-- so the stream is ready for stanzas.  RFC 6120 Section 4.3
+			mark_connected(session);
+			return true;
+		elseif not session.dialback_verifying then
+			session.log("warn", "No SASL EXTERNAL offer and Dialback doesn't seem to be enabled, giving up");
+			session:close();
+			return false;
+		end
+	end, -1);
 end
 
 -- Stream is authorised, and ready for normal stanzas
 function mark_connected(session)
 	local sendq, send = session.sendq, session.sends2s;
-	
+
 	local from, to = session.from_host, session.to_host;
-	
-	session.log("info", "%s s2s connection %s->%s complete", session.direction, from, to);
+
+	session.log("info", "%s s2s connection %s->%s complete", session.direction:gsub("^.", string.upper), from, to);
 
 	local event_data = { session = session };
 	if session.type == "s2sout" then
@@ -166,7 +183,7 @@
 		fire_global_event("s2sin-established", event_data);
 		hosts[to].events.fire_event("s2sin-established", event_data);
 	end
-	
+
 	if session.direction == "outgoing" then
 		if sendq then
 			session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host);
@@ -176,7 +193,7 @@
 			end
 			session.sendq = nil;
 		end
-		
+
 		session.ip_hosts = nil;
 		session.srv_hosts = nil;
 	end
@@ -211,14 +228,17 @@
 		return false;
 	end
 	session.log("debug", "connection %s->%s is now authenticated for %s", session.from_host, session.to_host, host);
-	
-	mark_connected(session);
-	
+
+	if (session.type == "s2sout" and session.external_auth ~= "succeeded") or session.type == "s2sin" then
+		-- Stream either used dialback for authentication or is an incoming stream.
+		mark_connected(session);
+	end
+
 	return true;
 end
 
 --- Helper to check that a session peer's certificate is valid
-local function check_cert_status(session)
+function check_cert_status(session)
 	local host = session.direction == "outgoing" and session.to_host or session.from_host
 	local conn = session.conn:socket()
 	local cert
@@ -226,39 +246,6 @@
 		cert = conn:getpeercertificate()
 	end
 
-	if cert then
-		local chain_valid, errors;
-		if conn.getpeerverification then
-			chain_valid, errors = conn:getpeerverification();
-		elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
-			chain_valid, errors = conn:getpeerchainvalid();
-			errors = (not chain_valid) and { { errors } } or nil;
-		else
-			chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
-		end
-		-- Is there any interest in printing out all/the number of errors here?
-		if not chain_valid then
-			(session.log or log)("debug", "certificate chain validation result: invalid");
-			for depth, t in pairs(errors or NULL) do
-				(session.log or log)("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
-			end
-			session.cert_chain_status = "invalid";
-		else
-			(session.log or log)("debug", "certificate chain validation result: valid");
-			session.cert_chain_status = "valid";
-
-			-- 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
-				(session.log or log)("debug", "certificate identity validation result: %s", session.cert_identity_status);
-			end
-		end
-	end
 	return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert });
 end
 
@@ -270,25 +257,28 @@
 
 function stream_callbacks.streamopened(session, attr)
 	local send = session.sends2s;
-	
+
 	session.version = tonumber(attr.version) or 0;
-	
+
 	-- TODO: Rename session.secure to session.encrypted
 	if session.secure == false then
 		session.secure = true;
+		session.encrypted = true;
 
-		-- Check if TLS compression is used
 		local sock = session.conn:socket();
 		if sock.info then
-			session.compressed = sock:info"compression";
-		elseif sock.compression then
-			session.compressed = sock:compression(); --COMPAT mw/luasec-hg
+			local info = sock:info();
+			(session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher);
+			session.compressed = info.compression;
+		else
+			(session.log or log)("info", "Stream encrypted");
+			session.compressed = sock.compression and sock:compression(); --COMPAT mw/luasec-hg
 		end
 	end
 
 	if session.direction == "incoming" then
 		-- Send a reply stream header
-		
+
 		-- Validate to/from
 		local to, from = nameprep(attr.to), nameprep(attr.from);
 		if not to and attr.to then -- COMPAT: Some servers do not reliably set 'to' (especially on stream restarts)
@@ -299,7 +289,7 @@
 			session:close({ condition = "improper-addressing", text = "Invalid 'from' address" });
 			return;
 		end
-		
+
 		-- Set session.[from/to]_host if they have not been set already and if
 		-- this session isn't already authenticated
 		if session.type == "s2sin_unauthed" and from and not session.from_host then
@@ -314,10 +304,10 @@
 			session:close({ condition = "improper-addressing", text = "New stream 'to' attribute does not match original" });
 			return;
 		end
-		
+
 		-- For convenience we'll put the sanitised values into these variables
 		to, from = session.to_host, session.from_host;
-		
+
 		session.streamid = uuid_gen();
 		(session.log or log)("debug", "Incoming s2s received %s", st.stanza("stream:stream", attr):top_tag());
 		if to then
@@ -352,13 +342,13 @@
 		session:open_stream(session.to_host, session.from_host)
 		if session.version >= 1.0 then
 			local features = st.stanza("stream:features");
-			
+
 			if to then
 				hosts[to].events.fire_event("s2s-stream-features", { origin = session, features = features });
 			else
 				(session.log or log)("warn", "No 'to' on stream header from %s means we can't offer any features", from or session.ip or "unknown host");
 			end
-			
+
 			log("debug", "Sending stream features: %s", tostring(features));
 			send(features);
 		end
@@ -391,7 +381,7 @@
 			end
 		end
 		session.send_buffer = nil;
-	
+
 		-- If server is pre-1.0, don't wait for features, just do dialback
 		if session.version < 1.0 then
 			if not session.dialback_verifying then
@@ -484,10 +474,10 @@
 
 		session.sends2s("</stream:stream>");
 		function session.sends2s() return false; end
-		
+
 		local reason = remote_reason or (reason and (reason.text or reason.condition)) or reason;
-		session.log("info", "%s s2s stream %s->%s closed: %s", session.direction, session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
-		
+		session.log("info", "%s s2s stream %s->%s closed: %s", session.direction:gsub("^.", string.upper), session.from_host or "(unknown host)", session.to_host or "(unknown host)", reason or "stream closed");
+
 		-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
 		local conn = session.conn;
 		if reason == nil and not session.notopen and session.type == "s2sin" then
@@ -505,47 +495,52 @@
 	end
 end
 
-function session_open_stream(session, from, to)
-	local attr = {
-		["xmlns:stream"] = 'http://etherx.jabber.org/streams',
-		xmlns = 'jabber:server',
-		version = session.version and (session.version > 0 and "1.0" or nil),
-		["xml:lang"] = 'en',
-		id = session.streamid,
-		from = from, to = to,
-	}
+function session_stream_attrs(session, from, to, attr)
 	if not from or (hosts[from] and hosts[from].modules.dialback) then
 		attr["xmlns:db"] = 'jabber:server:dialback';
 	end
-
-	session.sends2s("<?xml version='1.0'?>");
-	session.sends2s(st.stanza("stream:stream", attr):top_tag());
-	return true;
 end
 
 -- Session initialization logic shared by incoming and outgoing
 local function initialize_session(session)
 	local stream = new_xmpp_stream(session, stream_callbacks);
+	local log = session.log or log;
 	session.stream = stream;
-	
+
 	session.notopen = true;
-		
+
 	function session.reset_stream()
 		session.notopen = true;
 		session.streamid = nil;
 		session.stream:reset();
 	end
 
-	session.open_stream = session_open_stream;
-	
-	local filter = session.filter;
+	session.stream_attrs = session_stream_attrs;
+
+	local filter = initialize_filters(session);
+	local conn = session.conn;
+	local w = conn.write;
+
+	function session.sends2s(t)
+		log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^[^>]*>?"));
+		if t.name then
+			t = filter("stanzas/out", t);
+		end
+		if t then
+			t = filter("bytes/out", tostring(t));
+			if t then
+				return w(conn, t);
+			end
+		end
+	end
+
 	function session.data(data)
 		data = filter("bytes/in", data);
 		if data then
 			local ok, err = stream:feed(data);
 			if ok then return; end
-			(session.log or log)("warn", "Received invalid XML: %s", data);
-			(session.log or log)("warn", "Problem was: %s", err);
+			log("warn", "Received invalid XML: %s", data);
+			log("warn", "Problem was: %s", err);
 			session:close("not-well-formed");
 		end
 	end
@@ -557,6 +552,8 @@
 		return handlestanza(session, stanza);
 	end
 
+	module:fire_event("s2s-created", { session = session });
+
 	add_task(connect_timeout, function ()
 		if session.type == "s2sin" or session.type == "s2sout" then
 			return; -- Ok, we're connected
@@ -577,26 +574,11 @@
 		session = s2s_new_incoming(conn);
 		sessions[conn] = session;
 		session.log("debug", "Incoming s2s connection");
-
-		local filter = initialize_filters(session);
-		local w = conn.write;
-		session.sends2s = function (t)
-			log("debug", "sending: %s", t.top_tag and t:top_tag() or t:match("^([^>]*>?)"));
-			if t.name then
-				t = filter("stanzas/out", t);
-			end
-			if t then
-				t = filter("bytes/out", tostring(t));
-				if t then
-					return w(conn, t);
-				end
-			end
-		end
-	
 		initialize_session(session);
 	else -- Outgoing session connected
 		session:open_stream(session.from_host, session.to_host);
 	end
+	session.ip = conn:ip();
 end
 
 function listener.onincoming(conn, data)
@@ -605,7 +587,7 @@
 		session.data(data);
 	end
 end
-	
+
 function listener.onstatus(conn, status)
 	if status == "ssl-handshake-complete" then
 		local session = sessions[conn];
@@ -623,7 +605,6 @@
 		if err and session.direction == "outgoing" and session.notopen then
 			(session.log or log)("debug", "s2s connection attempt failed: %s", err);
 			if s2sout.attempt_connection(session, err) then
-				(session.log or log)("debug", "...so we're going to try another target");
 				return; -- Session lives for now
 			end
 		end
@@ -632,8 +613,14 @@
 	end
 end
 
+function listener.onreadtimeout(conn)
+	local session = sessions[conn];
+	if session then
+		return (hosts[session.host] or prosody).events.fire_event("s2s-read-timeout", { session = session });
+	end
+end
+
 function listener.register_outgoing(conn, session)
-	session.direction = "outgoing";
 	sessions[conn] = session;
 	initialize_session(session);
 end
@@ -651,7 +638,7 @@
 	elseif must_secure and insecure_domains[host] then
 		must_secure = false;
 	end
-	
+
 	if must_secure and (session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid") then
 		module:log("warn", "Forbidding insecure connection to/from %s", host or session.ip or "(unknown host)");
 		if session.direction == "incoming" then
--- a/plugins/mod_s2s/s2sout.lib.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_s2s/s2sout.lib.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -47,14 +47,14 @@
 function s2sout.initiate_connection(host_session)
 	initialize_filters(host_session);
 	host_session.version = 1;
-	
+
 	-- Kick the connection attempting machine into life
 	if not s2sout.attempt_connection(host_session) then
 		-- Intentionally not returning here, the
 		-- session is needed, connected or not
 		s2s_destroy_session(host_session);
 	end
-	
+
 	if not host_session.sends2s then
 		-- A sends2s which buffers data (until the stream is opened)
 		-- note that data in this buffer will be sent before the stream is authed
@@ -75,11 +75,11 @@
 function s2sout.attempt_connection(host_session, err)
 	local to_host = host_session.to_host;
 	local connect_host, connect_port = to_host and idna_to_ascii(to_host), 5269;
-	
+
 	if not connect_host then
 		return false;
 	end
-	
+
 	if not err then -- This is our first attempt
 		log("debug", "First attempt to connect to %s, starting with SRV lookup...", to_host);
 		host_session.connecting = true;
@@ -100,7 +100,7 @@
 					return;
 				end
 				t_sort(srv_hosts, compare_srv_priorities);
-				
+
 				local srv_choice = srv_hosts[1];
 				host_session.srv_choice = 1;
 				if srv_choice then
@@ -119,7 +119,7 @@
 				end
 			end
 		end, "_xmpp-server._tcp."..connect_host..".", "SRV");
-		
+
 		return true; -- Attempt in progress
 	elseif host_session.ip_hosts then
 		return s2sout.try_connect(host_session, connect_host, connect_port, err);
@@ -129,11 +129,11 @@
 		connect_host, connect_port = srv_choice.target or to_host, srv_choice.port or connect_port;
 		host_session.log("info", "Connection failed (%s). Attempt #%d: This time to %s:%d", tostring(err), host_session.srv_choice, connect_host, connect_port);
 	else
-		host_session.log("info", "Out of connection options, can't connect to %s", tostring(host_session.to_host));
+		host_session.log("info", "Failed in all attempts to connect to %s", tostring(host_session.to_host));
 		-- We're out of options
 		return false;
 	end
-	
+
 	if not (connect_host and connect_port) then
 		-- Likely we couldn't resolve DNS
 		log("warn", "Hmm, we're without a host (%s) and port (%s) to connect to for %s, giving up :(", tostring(connect_host), tostring(connect_port), tostring(to_host));
@@ -265,11 +265,12 @@
 end
 
 function s2sout.make_connect(host_session, connect_host, connect_port)
-	(host_session.log or log)("info", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
+	(host_session.log or log)("debug", "Beginning new connection attempt to %s ([%s]:%d)", host_session.to_host, connect_host.addr, connect_port);
 
 	-- Reset secure flag in case this is another
 	-- connection attempt after a failed STARTTLS
 	host_session.secure = nil;
+	host_session.encrypted = nil;
 
 	local conn, handler;
 	local proto = connect_host.proto;
@@ -280,7 +281,7 @@
 	else
 		handler = "Unsupported protocol: "..tostring(proto);
 	end
-	
+
 	if not conn then
 		log("warn", "Failed to create outgoing connection, system error: %s", handler);
 		return false, handler;
@@ -292,29 +293,14 @@
 		log("warn", "s2s connect() to %s (%s:%d) failed: %s", host_session.to_host, connect_host.addr, connect_port, err);
 		return false, err;
 	end
-	
+
 	conn = wrapclient(conn, connect_host.addr, connect_port, s2s_listener, "*a");
 	host_session.conn = conn;
-	
-	local filter = initialize_filters(host_session);
-	local w, log = conn.write, host_session.log;
-	host_session.sends2s = function (t)
-		log("debug", "sending: %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
-		if t.name then
-			t = filter("stanzas/out", t);
-		end
-		if t then
-			t = filter("bytes/out", tostring(t));
-			if t then
-				return w(conn, tostring(t));
-			end
-		end
-	end
-	
+
 	-- Register this outgoing connection so that xmppserver_listener knows about it
 	-- otherwise it will assume it is a new incoming connection
 	s2s_listener.register_outgoing(conn, host_session);
-	
+
 	log("debug", "Connection attempt in progress...");
 	return true;
 end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_s2s_auth_certs.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,49 @@
+module:set_global();
+
+local cert_verify_identity = require "util.x509".verify_identity;
+local NULL = {};
+local log = module._log;
+
+module:hook("s2s-check-certificate", function(event)
+	local session, host, cert = event.session, event.host, event.cert;
+	local conn = session.conn:socket();
+	local log = session.log or log;
+
+	if not cert then
+		log("warn", "No certificate provided by %s", host or "unknown host");
+		return;
+	end
+
+	local chain_valid, errors;
+	if conn.getpeerverification then
+		chain_valid, errors = conn:getpeerverification();
+	elseif conn.getpeerchainvalid then -- COMPAT mw/luasec-hg
+		chain_valid, errors = conn:getpeerchainvalid();
+		errors = (not chain_valid) and { { errors } } or nil;
+	else
+		chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } };
+	end
+	-- Is there any interest in printing out all/the number of errors here?
+	if not chain_valid then
+		log("debug", "certificate chain validation result: invalid");
+		for depth, t in pairs(errors or NULL) do
+			log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", "))
+		end
+		session.cert_chain_status = "invalid";
+	else
+		log("debug", "certificate chain validation result: valid");
+		session.cert_chain_status = "valid";
+
+		-- 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
+			log("debug", "certificate identity validation result: %s", session.cert_identity_status);
+		end
+	end
+end, 509);
+
--- a/plugins/mod_saslauth.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_saslauth.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -13,13 +13,13 @@
 local sm_make_authenticated = require "core.sessionmanager".make_authenticated;
 local base64 = require "util.encodings".base64;
 
-local cert_verify_identity = require "util.x509".verify_identity;
-
 local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler;
 local tostring = tostring;
 
-local secure_auth_only = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
-local allow_unencrypted_plain_auth = module:get_option("allow_unencrypted_plain_auth")
+local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", false));
+local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false)
+local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"});
+local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", {});
 
 local log = module._log;
 
@@ -28,15 +28,15 @@
 
 local function build_reply(status, ret, err_msg)
 	local reply = st.stanza(status, {xmlns = xmlns_sasl});
-	if status == "challenge" then
-		--log("debug", "CHALLENGE: %s", ret or "");
-		reply:text(base64.encode(ret or ""));
-	elseif status == "failure" then
+	if status == "failure" then
 		reply:tag(ret):up();
 		if err_msg then reply:tag("text"):text(err_msg); end
-	elseif status == "success" then
-		--log("debug", "SUCCESS: %s", ret or "");
-		reply:text(base64.encode(ret or ""));
+	elseif status == "challenge" or status == "success" then
+		if ret == "" then
+			reply:text("=")
+		elseif ret then
+			reply:text(base64.encode(ret));
+		end
 	else
 		module:log("error", "Unknown sasl status: %s", status);
 	end
@@ -99,12 +99,10 @@
 	module:log("info", "SASL EXTERNAL with %s failed", session.to_host)
 	-- TODO: Log the failure reason
 	session.external_auth = "failed"
+	session:close();
+	return true;
 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
 
@@ -124,71 +122,52 @@
 end, 150);
 
 local function s2s_external_auth(session, stanza)
+	if session.external_auth ~= "offered" then return end -- Unexpected request
+
 	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
+	if mechanism ~= "EXTERNAL" then
+		session.sends2s(build_reply("failure", "invalid-mechanism"));
 		return true;
 	end
 
-	if mechanism ~= "EXTERNAL" or session.cert_chain_status ~= "valid" then
-		session.sends2s(build_reply("failure", "invalid-mechanism"))
+	if not session.secure then
+		session.sends2s(build_reply("failure", "encryption-required"));
 		return true;
 	end
 
-	local text = stanza[1]
+	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"))
+		session.sends2s(build_reply("failure", "malformed-request"));
 		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
+	text = base64.decode(text);
+	if not text then
+		session.sends2s(build_reply("failure", "incorrect-encoding"));
+		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
+	-- The text value is either "" or equals session.from_host
+	if not ( text == "" or text == session.from_host ) then
+		session.sends2s(build_reply("failure", "invalid-authzid"));
+		return true;
 	end
 
-	session.external_auth = "succeeded"
-
-	if not session.from_host then
-		session.from_host = text;
+	-- We've already verified the external cert identity before offering EXTERNAL
+	if session.cert_chain_status ~= "valid" or session.cert_identity_status ~= "valid" then
+		session.sends2s(build_reply("failure", "not-authorized"));
+		session:close();
+		return true;
 	end
-	session.sends2s(build_reply("success"))
 
-	local domain = text ~= "" and text or session.from_host;
-	module:log("info", "Accepting SASL EXTERNAL identity from %s", domain);
-	module:fire_event("s2s-authenticated", { session = session, host = domain });
+	-- Success!
+	session.external_auth = "succeeded";
+	session.sends2s(build_reply("success"));
+	module:log("info", "Accepting SASL EXTERNAL identity from %s", session.from_host);
+	module:fire_event("s2s-authenticated", { session = session, host = session.from_host });
 	session:reset_stream();
-	return true
+	return true;
 end
 
 module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event)
@@ -206,9 +185,12 @@
 		session.sasl_handler = usermanager_get_sasl_handler(module.host, session);
 	end
 	local mechanism = stanza.attr.mechanism;
-	if not session.secure and (secure_auth_only or (mechanism == "PLAIN" and not allow_unencrypted_plain_auth)) then
+	if not session.secure and (secure_auth_only or insecure_mechanisms:contains(mechanism)) then
 		session.send(build_reply("failure", "encryption-required"));
 		return true;
+	elseif disabled_mechanisms:contains(mechanism) then
+		session.send(build_reply("failure", "invalid-mechanism"));
+		return true;
 	end
 	local valid_mechanism = session.sasl_handler:select(mechanism);
 	if not valid_mechanism then
@@ -232,6 +214,10 @@
 	return true;
 end);
 
+local function tls_unique(self)
+	return self.userdata["tls-unique"]:getpeerfinished();
+end
+
 local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' };
 local bind_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-bind' };
 local xmpp_session_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-session' };
@@ -241,14 +227,32 @@
 		if secure_auth_only and not origin.secure then
 			return;
 		end
-		origin.sasl_handler = usermanager_get_sasl_handler(module.host, origin);
+		local sasl_handler = usermanager_get_sasl_handler(module.host, origin)
+		origin.sasl_handler = sasl_handler;
+		if origin.encrypted then
+			-- check wether LuaSec has the nifty binding to the function needed for tls-unique
+			-- FIXME: would be nice to have this check only once and not for every socket
+			if sasl_handler.add_cb_handler then
+				local socket = origin.conn:socket();
+				if socket.getpeerfinished then
+					sasl_handler:add_cb_handler("tls-unique", tls_unique);
+				end
+				sasl_handler["userdata"] = {
+					["tls-unique"] = socket;
+				};
+			end
+		end
 		local mechanisms = st.stanza("mechanisms", mechanisms_attr);
-		for mechanism in pairs(origin.sasl_handler:mechanisms()) do
-			if mechanism ~= "PLAIN" or origin.secure or allow_unencrypted_plain_auth then
+		for mechanism in pairs(sasl_handler:mechanisms()) do
+			if (not disabled_mechanisms:contains(mechanism)) and (origin.secure or not insecure_mechanisms:contains(mechanism)) then
 				mechanisms:tag("mechanism"):text(mechanism):up();
 			end
 		end
-		if mechanisms[1] then features:add_child(mechanisms); end
+		if mechanisms[1] then
+			features:add_child(mechanisms);
+		else
+			(origin.log or log)("warn", "No SASL mechanisms to offer");
+		end
 	else
 		features:tag("bind", bind_attr):tag("required"):up():up();
 		features:tag("session", xmpp_session_attr):tag("optional"):up():up();
@@ -258,10 +262,10 @@
 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")
+		-- Offer EXTERNAL only if both chain and identity is valid.
+		if origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then
+			module:log("debug", "Offering SASL EXTERNAL");
+			origin.external_auth = "offered"
 			features:tag("mechanisms", { xmlns = xmlns_sasl })
 				:tag("mechanism"):text("EXTERNAL")
 			:up():up();
@@ -274,7 +278,7 @@
 	local resource;
 	if stanza.attr.type == "set" then
 		local bind = stanza.tags[1];
-		resource = bind:child_with_name("resource");
+		resource = bind:get_child("resource");
 		resource = resource and #resource.tags == 0 and resource[1] or nil;
 	end
 	local success, err_type, err, err_msg = sm_bind_resource(origin, resource);
--- a/plugins/mod_storage_internal.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_storage_internal.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -6,6 +6,9 @@
 local driver_mt = { __index = driver };
 
 function driver:open(store, typ)
+	if typ and typ ~= "keyval" then
+		return nil, "unsupported-store";
+	end
 	return setmetatable({ store = store, type = typ }, driver_mt);
 end
 function driver:get(user)
--- a/plugins/mod_storage_none.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_storage_none.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,8 +1,11 @@
 local driver = {};
 local driver_mt = { __index = driver };
 
-function driver:open(store)
-	return setmetatable({ store = store }, driver_mt);
+function driver:open(store, typ)
+	if typ and typ ~= "keyval" then
+		return nil, "unsupported-store";
+	end
+	return setmetatable({ store = store, type = typ }, driver_mt);
 end
 function driver:get(user)
 	return {};
--- a/plugins/mod_storage_sql.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_storage_sql.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -49,7 +49,7 @@
 end
 
 
-local resolve_relative_path = require "core.configmanager".resolve_relative_path;
+local resolve_relative_path = require "util.paths".resolve_relative_path;
 
 local function test_connection()
 	if not connection then return nil; end
@@ -93,7 +93,7 @@
 	elseif params.driver == "MySQL" then
 		create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
 	end
-	
+
 	local stmt, err = connection:prepare(create_sql);
 	if stmt then
 		local ok = stmt:execute();
@@ -159,18 +159,18 @@
 	end
 
 	params = params or { driver = "SQLite3" };
-	
+
 	if params.driver == "SQLite3" then
 		params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
 	end
-	
+
 	assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
 
 	dburi = db2uri(params);
 	connection = connections[dburi];
-	
+
 	assert(connect());
-	
+
 	-- Automatically create table, ignore failure (table probably already exists)
 	create_table();
 end
@@ -209,7 +209,7 @@
 	local ok, err = stmt:execute(...);
 	if not ok and not test_connection() then error("connection failed"); end
 	if not ok then return nil, err; end
-	
+
 	return stmt;
 end
 local function getsql(sql, ...)
@@ -236,7 +236,7 @@
 local function keyval_store_get()
 	local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
 	if not stmt then return rollback(nil, err); end
-	
+
 	local haveany;
 	local result = {};
 	for row in stmt:rows(true) do
@@ -256,7 +256,7 @@
 local function keyval_store_set(data)
 	local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?");
 	if not affected then return rollback(affected, err); end
-	
+
 	if data and next(data) ~= nil then
 		local extradata = {};
 		for key, value in pairs(data) do
@@ -314,7 +314,7 @@
 local function map_store_get(key)
 	local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
 	if not stmt then return rollback(nil, err); end
-	
+
 	local haveany;
 	local result = {};
 	for row in stmt:rows(true) do
@@ -334,7 +334,7 @@
 local function map_store_set(key, data)
 	local affected, err = setsql("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
 	if not affected then return rollback(affected, err); end
-	
+
 	if data and next(data) ~= nil then
 		if type(key) == "string" and key ~= "" then
 			local t, value = serialize(data);
@@ -365,25 +365,25 @@
 list_store.__index = list_store;
 function list_store:scan(username, from, to, jid, typ)
 	user,store = username,self.store;
-	
+
 	local cols = {"from", "to", "jid", "typ"};
 	local vals = { from ,  to ,  jid ,  typ };
 	local stmt, err;
 	local query = "SELECT * FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=?";
-	
+
 	query = query.." ORDER BY time";
 	--local stmt, err = getsql("SELECT * FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", key or "");
-	
+
 	return nil, "not-implemented"
 end
 
 local driver = {};
 
 function driver:open(store, typ)
-	if not typ then -- default key-value store
-		return setmetatable({ store = store }, keyval_store);
+	if typ and typ ~= "keyval" then
+		return nil, "unsupported-store";
 	end
-	return nil, "unsupported-store";
+	return setmetatable({ store = store }, keyval_store);
 end
 
 function driver:stores(username)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_storage_sql2.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,377 @@
+
+local json = require "util.json";
+local xml_parse = require "util.xml".parse;
+local uuid = require "util.uuid";
+local resolve_relative_path = require "util.paths".resolve_relative_path;
+
+local stanza_mt = require"util.stanza".stanza_mt;
+local getmetatable = getmetatable;
+local t_concat = table.concat;
+local function is_stanza(x) return getmetatable(x) == stanza_mt; end
+
+local noop = function() end
+local unpack = unpack
+local function iterator(result)
+	return function(result)
+		local row = result();
+		if row ~= nil then
+			return unpack(row);
+		end
+	end, result, nil;
+end
+
+local mod_sql = module:require("sql");
+local params = module:get_option("sql");
+
+local engine; -- TODO create engine
+
+local function create_table()
+	local Table,Column,Index = mod_sql.Table,mod_sql.Column,mod_sql.Index;
+
+	local ProsodyTable = Table {
+		name="prosody";
+		Column { name="host", type="TEXT", nullable=false };
+		Column { name="user", type="TEXT", nullable=false };
+		Column { name="store", type="TEXT", nullable=false };
+		Column { name="key", type="TEXT", nullable=false };
+		Column { name="type", type="TEXT", nullable=false };
+		Column { name="value", type="MEDIUMTEXT", nullable=false };
+		Index { name="prosody_index", "host", "user", "store", "key" };
+	};
+	engine:transaction(function()
+		ProsodyTable:create(engine);
+	end);
+
+	local ProsodyArchiveTable = Table {
+		name="prosodyarchive";
+		Column { name="sort_id", type="INTEGER", primary_key=true, auto_increment=true };
+		Column { name="host", type="TEXT", nullable=false };
+		Column { name="user", type="TEXT", nullable=false };
+		Column { name="store", type="TEXT", nullable=false };
+		Column { name="key", type="TEXT", nullable=false }; -- item id
+		Column { name="when", type="INTEGER", nullable=false }; -- timestamp
+		Column { name="with", type="TEXT", nullable=false }; -- related id
+		Column { name="type", type="TEXT", nullable=false };
+		Column { name="value", type="MEDIUMTEXT", nullable=false };
+		Index { name="prosodyarchive_index", unique = true, "host", "user", "store", "key" };
+	};
+	engine:transaction(function()
+		ProsodyArchiveTable:create(engine);
+	end);
+end
+
+local function upgrade_table()
+	if params.driver == "MySQL" then
+		local success,err = engine:transaction(function()
+			local result = engine:execute("SHOW COLUMNS FROM prosody WHERE Field='value' and Type='text'");
+			if result:rowcount() > 0 then
+				module:log("info", "Upgrading database schema...");
+				engine:execute("ALTER TABLE prosody MODIFY COLUMN `value` MEDIUMTEXT");
+				module:log("info", "Database table automatically upgraded");
+			end
+			return true;
+		end);
+		if not success then
+			module:log("error", "Failed to check/upgrade database schema (%s), please see "
+				.."http://prosody.im/doc/mysql for help",
+				err or "unknown error");
+			return false;
+		end
+		-- COMPAT w/pre-0.9: Upgrade tables to UTF-8 if not already
+		local check_encoding_query = "SELECT `COLUMN_NAME`,`COLUMN_TYPE` FROM `information_schema`.`columns` WHERE `TABLE_NAME`='prosody' AND ( `CHARACTER_SET_NAME`!='utf8' OR `COLLATION_NAME`!='utf8_bin' );";
+		success,err = engine:transaction(function()
+			local result = engine:execute(check_encoding_query);
+			local n_bad_columns = result:rowcount();
+			if n_bad_columns > 0 then
+				module:log("warn", "Found %d columns in prosody table requiring encoding change, updating now...", n_bad_columns);
+				local fix_column_query1 = "ALTER TABLE `prosody` CHANGE `%s` `%s` BLOB;";
+				local fix_column_query2 = "ALTER TABLE `prosody` CHANGE `%s` `%s` %s CHARACTER SET 'utf8' COLLATE 'utf8_bin';";
+				for row in result:rows() do
+					local column_name, column_type = unpack(row);
+					engine:execute(fix_column_query1:format(column_name, column_name));
+					engine:execute(fix_column_query2:format(column_name, column_name, column_type));
+				end
+				module:log("info", "Database encoding upgrade complete!");
+			end
+		end);
+		success,err = engine:transaction(function() return engine:execute(check_encoding_query); end);
+		if not success then
+			module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error");
+		end
+	end
+end
+
+do -- process options to get a db connection
+	params = params or { driver = "SQLite3" };
+
+	if params.driver == "SQLite3" then
+		params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
+	end
+
+	assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
+
+	--local dburi = db2uri(params);
+	engine = mod_sql:create_engine(params);
+
+	if module:get_option("sql_manage_tables", true) then
+		-- Automatically create table, ignore failure (table probably already exists)
+		create_table();
+		-- Encoding mess
+		upgrade_table();
+	end
+end
+
+local function serialize(value)
+	local t = type(value);
+	if t == "string" or t == "boolean" or t == "number" then
+		return t, tostring(value);
+	elseif is_stanza(value) then
+		return "xml", tostring(value);
+	elseif t == "table" then
+		local value,err = json.encode(value);
+		if value then return "json", value; end
+		return nil, err;
+	end
+	return nil, "Unhandled value type: "..t;
+end
+local function deserialize(t, value)
+	if t == "string" then return value;
+	elseif t == "boolean" then
+		if value == "true" then return true;
+		elseif value == "false" then return false; end
+	elseif t == "number" then return tonumber(value);
+	elseif t == "json" then
+		return json.decode(value);
+	elseif t == "xml" then
+		return xml_parse(value);
+	end
+end
+
+local host = module.host;
+local user, store;
+
+local function keyval_store_get()
+	local haveany;
+	local result = {};
+	for row in engine:select("SELECT `key`,`type`,`value` FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store) do
+		haveany = true;
+		local k = row[1];
+		local v = deserialize(row[2], row[3]);
+		if k and v then
+			if k ~= "" then result[k] = v; elseif type(v) == "table" then
+				for a,b in pairs(v) do
+					result[a] = b;
+				end
+			end
+		end
+	end
+	if haveany then
+		return result;
+	end
+end
+local function keyval_store_set(data)
+	engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=? AND `store`=?", host, user or "", store);
+
+	if data and next(data) ~= nil then
+		local extradata = {};
+		for key, value in pairs(data) do
+			if type(key) == "string" and key ~= "" then
+				local t, value = serialize(value);
+				assert(t, value);
+				engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, key, t, value);
+			else
+				extradata[key] = value;
+			end
+		end
+		if next(extradata) ~= nil then
+			local t, extradata = serialize(extradata);
+			assert(t, extradata);
+			engine:insert("INSERT INTO `prosody` (`host`,`user`,`store`,`key`,`type`,`value`) VALUES (?,?,?,?,?,?)", host, user or "", store, "", t, extradata);
+		end
+	end
+	return true;
+end
+
+local keyval_store = {};
+keyval_store.__index = keyval_store;
+function keyval_store:get(username)
+	user,store = username,self.store;
+	local ok, result = engine:transaction(keyval_store_get);
+	if not ok then return ok, result; end
+	return result;
+end
+function keyval_store:set(username, data)
+	user,store = username,self.store;
+	return engine:transaction(function()
+		return keyval_store_set(data);
+	end);
+end
+function keyval_store:users()
+	local ok, result = engine:transaction(function()
+		return engine:select("SELECT DISTINCT `user` FROM `prosody` WHERE `host`=? AND `store`=?", host, self.store);
+	end);
+	if not ok then return ok, result end
+	return iterator(result);
+end
+
+local archive_store = {}
+archive_store.__index = archive_store
+function archive_store:append(username, key, when, with, value)
+	if value == nil then -- COMPAT early versions
+		when, with, value, key = key, when, with, value
+	end
+	local user,store = username,self.store;
+	return engine:transaction(function()
+		if key then
+			engine:delete("DELETE FROM `prosodyarchive` WHERE `host`=? AND `user`=? AND `store`=? AND `key`=?", host, user or "", store, key);
+		else
+			key = uuid.generate();
+		end
+		local t, value = serialize(value);
+		engine:insert("INSERT INTO `prosodyarchive` (`host`, `user`, `store`, `when`, `with`, `key`, `type`, `value`) VALUES (?,?,?,?,?,?,?,?)", host, user or "", store, when, with, key, t, value);
+		return key;
+	end);
+end
+
+-- Helpers for building the WHERE clause
+local function archive_where(query, args, where)
+	-- Time range, inclusive
+	if query.start then
+		args[#args+1] = query.start
+		where[#where+1] = "`when` >= ?"
+	end
+
+	if query["end"] then
+		args[#args+1] = query["end"];
+		if query.start then
+			where[#where] = "`when` BETWEEN ? AND ?" -- is this inclusive?
+		else
+			where[#where+1] = "`when` <= ?"
+		end
+	end
+
+	-- Related name
+	if query.with then
+		where[#where+1] = "`with` = ?";
+		args[#args+1] = query.with
+	end
+
+	-- Unique id
+	if query.key then
+		where[#where+1] = "`key` = ?";
+		args[#args+1] = query.key
+	end
+end
+local function archive_where_id_range(query, args, where)
+	local args_len = #args
+	-- Before or after specific item, exclusive
+	if query.after then  -- keys better be unique!
+		where[#where+1] = "`sort_id` > (SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? AND `host` = ? AND `user` = ? AND `store` = ? LIMIT 1)"
+		args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.after, args[1], args[2], args[3];
+		args_len = args_len + 4
+	end
+	if query.before then
+		where[#where+1] = "`sort_id` < (SELECT `sort_id` FROM `prosodyarchive` WHERE `key` = ? AND `host` = ? AND `user` = ? AND `store` = ? LIMIT 1)"
+		args[args_len+1], args[args_len+2], args[args_len+3], args[args_len+4] = query.before, args[1], args[2], args[3];
+	end
+end
+
+function archive_store:find(username, query)
+	query = query or {};
+	local user,store = username,self.store;
+	local total;
+	local ok, result = engine:transaction(function()
+		local sql_query = "SELECT `key`, `type`, `value`, `when` FROM `prosodyarchive` WHERE %s ORDER BY `sort_id` %s%s;";
+		local args = { host, user or "", store, };
+		local where = { "`host` = ?", "`user` = ?", "`store` = ?", };
+
+		archive_where(query, args, where);
+
+		-- Total matching
+		if query.total then
+			local stats = engine:select("SELECT COUNT(*) FROM `prosodyarchive` WHERE " .. t_concat(where, " AND "), unpack(args));
+			if stats then
+				local _total = stats()
+				total = _total and _total[1];
+			end
+			if query.limit == 0 then -- Skip the real query
+				return noop, total;
+			end
+		end
+
+		archive_where_id_range(query, args, where);
+
+		if query.limit then
+			args[#args+1] = query.limit;
+		end
+
+		sql_query = sql_query:format(t_concat(where, " AND "), query.reverse and "DESC" or "ASC", query.limit and " LIMIT ?" or "");
+		module:log("debug", sql_query);
+		return engine:select(sql_query, unpack(args));
+	end);
+	if not ok then return ok, result end
+	return function()
+		local row = result();
+		if row ~= nil then
+			return row[1], deserialize(row[2], row[3]), row[4];
+		end
+	end, total;
+end
+
+function archive_store:delete(username, query)
+	query = query or {};
+	local user,store = username,self.store;
+	return engine:transaction(function()
+		local sql_query = "DELETE FROM `prosodyarchive` WHERE %s;";
+		local args = { host, user or "", store, };
+		local where = { "`host` = ?", "`user` = ?", "`store` = ?", };
+		if user == true then
+			table.remove(args, 2);
+			table.remove(where, 2);
+		end
+		archive_where(query, args, where);
+		archive_where_id_range(query, args, where);
+		sql_query = sql_query:format(t_concat(where, " AND "));
+		module:log("debug", sql_query);
+		return engine:delete(sql_query, unpack(args));
+	end);
+end
+
+local stores = {
+	keyval = keyval_store;
+	archive = archive_store;
+};
+
+local driver = {};
+
+function driver:open(store, typ)
+	local store_mt = stores[typ or "keyval"];
+	if store_mt then
+		return setmetatable({ store = store }, store_mt);
+	end
+	return nil, "unsupported-store";
+end
+
+function driver:stores(username)
+	local sql = "SELECT DISTINCT `store` FROM `prosody` WHERE `host`=? AND `user`" ..
+		(username == true and "!=?" or "=?");
+	if username == true or not username then
+		username = "";
+	end
+	local ok, result = engine:transaction(function()
+		return engine:select(sql, host, username);
+	end);
+	if not ok then return ok, result end
+	return iterator(result);
+end
+
+function driver:purge(username)
+	return engine:transaction(function()
+		local stmt,err = engine:delete("DELETE FROM `prosody` WHERE `host`=? AND `user`=?", host, username);
+		return true,err;
+	end);
+end
+
+module:provides("storage", driver);
+
+
--- a/plugins/mod_time.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_time.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/plugins/mod_tls.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_tls.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,16 +1,15 @@
 -- 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 create_context = require "core.certmanager".create_context;
 local st = require "util.stanza";
 
-local c2s_require_encryption = module:get_option("c2s_require_encryption") or module:get_option("require_encryption");
+local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption"));
 local s2s_require_encryption = module:get_option("s2s_require_encryption");
 local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false;
 local s2s_secure_auth = module:get_option("s2s_secure_auth");
@@ -29,20 +28,56 @@
 if c2s_require_encryption then c2s_feature:tag("required"):up(); end
 if s2s_require_encryption then s2s_feature:tag("required"):up(); end
 
-local global_ssl_ctx = prosody.global_ssl_ctx;
-
 local hosts = prosody.hosts;
 local host = hosts[module.host];
 
+local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin;
+local ssl_cfg_c2s, ssl_cfg_s2sout, ssl_cfg_s2sin;
+do
+	local NULL, err = {};
+	local global = module:context("*");
+	local parent = module:context(module.host:match("%.(.*)$"));
+
+	local parent_ssl = parent:get_option("ssl");
+	local host_ssl   = module:get_option("ssl", parent_ssl);
+
+	local global_c2s = global:get_option("c2s_ssl", NULL);
+	local parent_c2s = parent:get_option("c2s_ssl", NULL);
+	local host_c2s   = module:get_option("c2s_ssl", parent_c2s);
+
+	local global_s2s = global:get_option("s2s_ssl", NULL);
+	local parent_s2s = parent:get_option("s2s_ssl", NULL);
+	local host_s2s   = module:get_option("s2s_ssl", parent_s2s);
+
+	ssl_ctx_c2s, err, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections
+	if not ssl_ctx_c2s then module:log("error", "Error creating context for c2s: %s", err); end
+
+	ssl_ctx_s2sout, err, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s); -- for outgoing server connections
+	if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err); end
+
+	ssl_ctx_s2sin, err, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s); -- for incoming server connections
+	if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err); end
+end
+
 local function can_do_tls(session)
+	if not session.conn.starttls then
+		return false;
+	elseif session.ssl_ctx then
+		return true;
+	end
 	if session.type == "c2s_unauthed" then
-		return session.conn.starttls and host.ssl_ctx_in;
+		session.ssl_ctx = ssl_ctx_c2s;
+		session.ssl_cfg = ssl_cfg_c2s;
 	elseif session.type == "s2sin_unauthed" and allow_s2s_tls then
-		return session.conn.starttls and host.ssl_ctx_in;
+		session.ssl_ctx = ssl_ctx_s2sin;
+		session.ssl_cfg = ssl_cfg_s2sin;
 	elseif session.direction == "outgoing" and allow_s2s_tls then
-		return session.conn.starttls and host.ssl_ctx;
+		session.ssl_ctx = ssl_ctx_s2sout;
+		session.ssl_cfg = ssl_cfg_s2sout;
+	else
+		return false;
 	end
-	return false;
+	return session.ssl_ctx;
 end
 
 -- Hook <starttls/>
@@ -51,9 +86,7 @@
 	if can_do_tls(origin) then
 		(origin.sends2s or origin.send)(starttls_proceed);
 		origin:reset_stream();
-		local host = origin.to_host or origin.host;
-		local ssl_ctx = host and hosts[host].ssl_ctx_in or global_ssl_ctx;
-		origin.conn:starttls(ssl_ctx);
+		origin.conn:starttls(origin.ssl_ctx);
 		origin.log("debug", "TLS negotiation started for %s...", origin.type);
 		origin.secure = false;
 	else
@@ -81,7 +114,7 @@
 -- For s2sout connections, start TLS if we can
 module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza)
 	module:log("debug", "Received features element");
-	if can_do_tls(session) and stanza:child_with_ns(xmlns_starttls) then
+	if can_do_tls(session) and stanza:get_child("starttls", xmlns_starttls) then
 		module:log("debug", "%s is offering TLS, taking up the offer...", session.to_host);
 		session.sends2s("<starttls xmlns='"..xmlns_starttls.."'/>");
 		return true;
@@ -91,30 +124,7 @@
 module:hook_stanza(xmlns_starttls, "proceed", function (session, stanza)
 	module:log("debug", "Proceeding with TLS on s2sout...");
 	session:reset_stream();
-	local ssl_ctx = session.from_host and hosts[session.from_host].ssl_ctx or global_ssl_ctx;
-	session.conn:starttls(ssl_ctx);
+	session.conn:starttls(session.ssl_ctx);
 	session.secure = false;
 	return true;
 end);
-
-local function assert_log(ret, err)
-	if not ret then
-		module:log("error", "Unable to initialize TLS: %s", err);
-	end
-	return ret;
-end
-
-function module.load()
-	local ssl_config = config.rawget(module.host, "ssl");
-	if not ssl_config then
-		local base_host = module.host:match("%.(.*)");
-		ssl_config = config.get(base_host, "ssl");
-	end
-	host.ssl_ctx = assert_log(create_context(host.host, "client", ssl_config)); -- for outgoing connections
-	host.ssl_ctx_in = assert_log(create_context(host.host, "server", ssl_config)); -- for incoming connections
-end
-
-function module.unload()
-	host.ssl_ctx = nil;
-	host.ssl_ctx_in = nil;
-end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_unknown.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,4 @@
+-- Unknown platform stub
+module:set_global();
+
+-- TODO Do things that make sense if we don't know about the platform
--- a/plugins/mod_uptime.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_uptime.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/plugins/mod_vcard.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_vcard.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/plugins/mod_version.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_version.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/plugins/mod_watchregistrations.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_watchregistrations.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_websocket.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,301 @@
+-- Prosody IM
+-- Copyright (C) 2012-2014 Florian Zeitz
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+module:set_global();
+
+local add_filter = require "util.filters".add_filter;
+local sha1 = require "util.hashes".sha1;
+local base64 = require "util.encodings".base64.encode;
+local st = require "util.stanza";
+local parse_xml = require "util.xml".parse;
+local portmanager = require "core.portmanager";
+local sm_destroy_session = sessionmanager.destroy_session;
+local log = module._log;
+
+local websocket_frames = require"net.websocket.frames";
+local parse_frame = websocket_frames.parse;
+local build_frame = websocket_frames.build;
+local build_close = websocket_frames.build_close;
+local parse_close = websocket_frames.parse_close;
+
+local t_concat = table.concat;
+
+local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure");
+local cross_domain = module:get_option("cross_domain_websocket");
+if cross_domain then
+	if cross_domain == true then
+		cross_domain = "*";
+	elseif type(cross_domain) == "table" then
+		cross_domain = t_concat(cross_domain, ", ");
+	end
+	if type(cross_domain) ~= "string" then
+		cross_domain = nil;
+	end
+end
+
+local xmlns_framing = "urn:ietf:params:xml:ns:xmpp-framing";
+local xmlns_streams = "http://etherx.jabber.org/streams";
+local xmlns_client = "jabber:client";
+local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'};
+
+module:depends("c2s")
+local sessions = module:shared("c2s/sessions");
+local c2s_listener = portmanager.get_service("c2s").listener;
+
+--- Session methods
+local function session_open_stream(session)
+	local attr = {
+		xmlns = xmlns_framing,
+		version = "1.0",
+		id = session.streamid or "",
+		from = session.host
+	};
+	session.send(st.stanza("open", attr));
+end
+
+local function session_close(session, reason)
+	local log = session.log or log;
+	if session.conn then
+		if session.notopen then
+			session:open_stream();
+		end
+		if reason then -- nil == no err, initiated by us, false == initiated by client
+			local stream_error = st.stanza("stream:error");
+			if type(reason) == "string" then -- assume stream error
+				stream_error:tag(reason, {xmlns = 'urn:ietf:params:xml:ns:xmpp-streams' });
+			elseif type(reason) == "table" then
+				if reason.condition then
+					stream_error:tag(reason.condition, stream_xmlns_attr):up();
+					if reason.text then
+						stream_error:tag("text", stream_xmlns_attr):text(reason.text):up();
+					end
+					if reason.extra then
+						stream_error:add_child(reason.extra);
+					end
+				elseif reason.name then -- a stanza
+					stream_error = reason;
+				end
+			end
+			log("debug", "Disconnecting client, <stream:error> is: %s", tostring(stream_error));
+			session.send(stream_error);
+		end
+
+		session.send(st.stanza("close", { xmlns = xmlns_framing }));
+		function session.send() return false; end
+
+		local reason = (reason and (reason.name or reason.text or reason.condition)) or reason;
+		session.log("debug", "c2s stream for %s closed: %s", session.full_jid or ("<"..session.ip..">"), reason or "session closed");
+
+		-- Authenticated incoming stream may still be sending us stanzas, so wait for </stream:stream> from remote
+		local conn = session.conn;
+		if reason == nil and not session.notopen and session.type == "c2s" then
+			-- Grace time to process data from authenticated cleanly-closed stream
+			add_task(stream_close_timeout, function ()
+				if not session.destroyed then
+					session.log("warn", "Failed to receive a stream close response, closing connection anyway...");
+					sm_destroy_session(session, reason);
+					conn:write(build_close(1000, "Stream closed"));
+					conn:close();
+				end
+			end);
+		else
+			sm_destroy_session(session, reason);
+			conn:write(build_close(1000, "Stream closed"));
+			conn:close();
+		end
+	end
+end
+
+
+--- Filters
+local function filter_open_close(data)
+	if not data:find(xmlns_framing, 1, true) then return data; end
+
+	local oc = parse_xml(data);
+	if not oc then return data; end
+	if oc.attr.xmlns ~= xmlns_framing then return data; end
+	if oc.name == "close" then return "</stream:stream>"; end
+	if oc.name == "open" then
+		oc.name = "stream:stream";
+		oc.attr.xmlns = nil;
+		oc.attr["xmlns:stream"] = xmlns_streams;
+		return oc:top_tag();
+	end
+
+	return data;
+end
+function handle_request(event, path)
+	local request, response = event.request, event.response;
+	local conn = response.conn;
+
+	if not request.headers.sec_websocket_key then
+		response.headers.content_type = "text/html";
+		return [[<!DOCTYPE html><html><head><title>Websocket</title></head><body>
+			<p>It works! Now point your WebSocket client to this URL to connect to Prosody.</p>
+			</body></html>]];
+	end
+
+	local wants_xmpp = false;
+	(request.headers.sec_websocket_protocol or ""):gsub("([^,]*),?", function (proto)
+		if proto == "xmpp" then wants_xmpp = true; end
+	end);
+
+	if not wants_xmpp then
+		return 501;
+	end
+
+	local function websocket_close(code, message)
+		conn:write(build_close(code, message));
+		conn:close();
+	end
+
+	local dataBuffer;
+	local function handle_frame(frame)
+		local opcode = frame.opcode;
+		local length = frame.length;
+		module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
+
+		-- Error cases
+		if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
+			websocket_close(1002, "Reserved bits not zero");
+			return false;
+		end
+
+		if opcode == 0x8 then -- close frame
+			if length == 1 then
+				websocket_close(1002, "Close frame with payload, but too short for status code");
+				return false;
+			elseif length >= 2 then
+				local status_code = parse_close(frame.data)
+				if status_code < 1000 then
+					websocket_close(1002, "Closed with invalid status code");
+					return false;
+				elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
+					websocket_close(1002, "Closed with reserved status code");
+					return false;
+				end
+			end
+		end
+
+		if opcode >= 0x8 then
+			if length > 125 then -- Control frame with too much payload
+				websocket_close(1002, "Payload too large");
+				return false;
+			end
+
+			if not frame.FIN then -- Fragmented control frame
+				websocket_close(1002, "Fragmented control frame");
+				return false;
+			end
+		end
+
+		if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
+			websocket_close(1002, "Reserved opcode");
+			return false;
+		end
+
+		if opcode == 0x0 and not dataBuffer then
+			websocket_close(1002, "Unexpected continuation frame");
+			return false;
+		end
+
+		if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
+			websocket_close(1002, "Continuation frame expected");
+			return false;
+		end
+
+		-- Valid cases
+		if opcode == 0x0 then -- Continuation frame
+			dataBuffer[#dataBuffer+1] = frame.data;
+		elseif opcode == 0x1 then -- Text frame
+			dataBuffer = {frame.data};
+		elseif opcode == 0x2 then -- Binary frame
+			websocket_close(1003, "Only text frames are supported");
+			return;
+		elseif opcode == 0x8 then -- Close request
+			websocket_close(1000, "Goodbye");
+			return;
+		elseif opcode == 0x9 then -- Ping frame
+			frame.opcode = 0xA;
+			conn:write(build_frame(frame));
+			return "";
+		elseif opcode == 0xA then -- Pong frame
+			module:log("warn", "Received unexpected pong frame: " .. tostring(frame.data));
+			return "";
+		else
+			log("warn", "Received frame with unsupported opcode %i", opcode);
+			return "";
+		end
+
+		if frame.FIN then
+			local data = t_concat(dataBuffer, "");
+			dataBuffer = nil;
+			return data;
+		end
+		return "";
+	end
+
+	conn:setlistener(c2s_listener);
+	c2s_listener.onconnect(conn);
+
+	local session = sessions[conn];
+
+	session.secure = consider_websocket_secure or session.secure;
+
+	session.open_stream = session_open_stream;
+	session.close = session_close;
+
+	local frameBuffer = "";
+	add_filter(session, "bytes/in", function(data)
+		local cache = {};
+		frameBuffer = frameBuffer .. data;
+		local frame, length = parse_frame(frameBuffer);
+
+		while frame do
+			frameBuffer = frameBuffer:sub(length + 1);
+			local result = handle_frame(frame);
+			if not result then return; end
+			cache[#cache+1] = filter_open_close(result);
+			frame, length = parse_frame(frameBuffer);
+		end
+		return t_concat(cache, "");
+	end);
+
+	add_filter(session, "stanzas/out", function(stanza)
+		local attr = stanza.attr;
+		attr.xmlns = attr.xmlns or xmlns_client;
+		if stanza.name:find("^stream:") then
+			attr["xmlns:stream"] = attr["xmlns:stream"] or xmlns_streams;
+		end
+		return stanza;
+	end);
+
+	add_filter(session, "bytes/out", function(data)
+		return build_frame({ FIN = true, opcode = 0x01, data = tostring(data)});
+	end);
+
+	response.status_code = 101;
+	response.headers.upgrade = "websocket";
+	response.headers.connection = "Upgrade";
+	response.headers.sec_webSocket_accept = base64(sha1(request.headers.sec_websocket_key .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
+	response.headers.sec_webSocket_protocol = "xmpp";
+	response.headers.access_control_allow_origin = cross_domain;
+
+	return "";
+end
+
+function module.add_host(module)
+	module:depends("http");
+	module:provides("http", {
+		name = "websocket";
+		default_path = "xmpp-websocket";
+		route = {
+			["GET"] = handle_request;
+			["GET /"] = handle_request;
+		};
+	});
+end
--- a/plugins/mod_welcome.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/mod_welcome.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/mod_windows.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,4 @@
+-- Windows platform stub
+module:set_global();
+
+-- TODO Add Windows-specific things here
--- a/plugins/muc/mod_muc.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/muc/mod_muc.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,11 +1,12 @@
 -- 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 array = require "util.array";
 
 if module:get_host_type() ~= "component" then
 	error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
@@ -16,12 +17,15 @@
 if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end
 local restrict_room_creation = module:get_option("restrict_room_creation");
 if restrict_room_creation then
-	if restrict_room_creation == true then 
+	if restrict_room_creation == true then
 		restrict_room_creation = "admin";
 	elseif restrict_room_creation ~= "admin" and restrict_room_creation ~= "local" then
 		restrict_room_creation = nil;
 	end
 end
+local lock_rooms = module:get_option_boolean("muc_room_locking", false);
+local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300);
+
 local muclib = module:require "muc";
 local muc_new_room = muclib.new_room;
 local jid_split = require "util.jid".split;
@@ -40,12 +44,17 @@
 -- Configurable options
 muclib.set_max_history_length(module:get_option_number("max_history_messages"));
 
+module:depends("disco");
+module:add_identity("conference", "text", muc_name);
+module:add_feature("http://jabber.org/protocol/muc");
+
 local function is_admin(jid)
 	return um_is_admin(jid, module.host);
 end
 
-local _set_affiliation = muc_new_room.room_mt.set_affiliation;
-local _get_affiliation = muc_new_room.room_mt.get_affiliation;
+room_mt = muclib.room_mt; -- Yes, global.
+local _set_affiliation = room_mt.set_affiliation;
+local _get_affiliation = room_mt.get_affiliation;
 function muclib.room_mt:get_affiliation(jid)
 	if is_admin(jid) then return "owner"; end
 	return _get_affiliation(self, jid);
@@ -83,6 +92,16 @@
 	room.route_stanza = room_route_stanza;
 	room.save = room_save;
 	rooms[jid] = room;
+	if lock_rooms then
+		room.locked = true;
+		if lock_room_timeout and lock_room_timeout > 0 then
+			module:add_timer(lock_room_timeout, function ()
+				if room.locked then
+					room:destroy(); -- Not unlocked in time
+				end
+			end);
+		end
+	end
 	module:fire_event("muc-room-created", { room = room });
 	return room;
 end
@@ -107,20 +126,15 @@
 host_room.route_stanza = room_route_stanza;
 host_room.save = room_save;
 
-local function get_disco_info(stanza)
-	return st.iq({type='result', id=stanza.attr.id, from=muc_host, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
-		:tag("identity", {category='conference', type='text', name=muc_name}):up()
-		:tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
-end
-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");
+module:hook("host-disco-items", function(event)
+	local reply = event.reply;
+	module:log("debug", "host-disco-items called");
 	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
-	return reply; -- TODO cache disco reply
-end
+end);
 
 local function handle_to_domain(event)
 	local origin, stanza = event.origin, event.stanza;
@@ -129,11 +143,7 @@
 	if stanza.name == "iq" and type == "get" then
 		local xmlns = stanza.tags[1].attr.xmlns;
 		local node = stanza.tags[1].attr.node;
-		if xmlns == "http://jabber.org/protocol/disco#info" and not node then
-			origin.send(get_disco_info(stanza));
-		elseif xmlns == "http://jabber.org/protocol/disco#items" and not node then
-			origin.send(get_disco_items(stanza));
-		elseif xmlns == "http://jabber.org/protocol/muc#unique" then
+		if xmlns == "http://jabber.org/protocol/muc#unique" then
 			origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions
 		else
 			origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
@@ -220,7 +230,8 @@
 	if not saved then
 		local stanza = st.presence({type = "unavailable"})
 			:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
-				:tag("item", { affiliation='none', role='none' }):up();
+				:tag("item", { affiliation='none', role='none' }):up()
+				:tag("status", { code = "332"}):up();
 		for roomjid, room in pairs(rooms) do
 			shutdown_room(room, stanza);
 		end
@@ -229,3 +240,39 @@
 end
 module.unload = shutdown_component;
 module:hook_global("server-stopping", shutdown_component);
+
+-- Ad-hoc commands
+module:depends("adhoc")
+local t_concat = table.concat;
+local keys = require "util.iterators".keys;
+local adhoc_new = module:require "adhoc".new;
+local adhoc_initial = require "util.adhoc".new_initial_data_form;
+local dataforms_new = require "util.dataforms".new;
+
+local destroy_rooms_layout = dataforms_new {
+	title = "Destroy rooms";
+	instructions = "Select the rooms to destroy";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" };
+	{ name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"};
+};
+
+local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function()
+	return { rooms = array.collect(keys(rooms)):sort() };
+end, function(fields, errors)
+	if errors then
+		local errmsg = {};
+		for name, err in pairs(errors) do
+			errmsg[#errmsg + 1] = name .. ": " .. err;
+		end
+		return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
+	end
+	for _, room in ipairs(fields.rooms) do
+		rooms[room]:destroy();
+		rooms[room] = nil;
+	end
+	return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") };
+end);
+local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin");
+
+module:provides("adhoc", destroy_rooms_desc);
--- a/plugins/muc/muc.lib.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/plugins/muc/muc.lib.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -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
@@ -130,18 +107,21 @@
 	end
 	stanza.attr.to = to;
 	if historic then -- add to history
-		local history = self._data['history'];
-		if not history then history = {}; self._data['history'] = history; end
-		stanza = st.clone(stanza);
-		stanza.attr.to = "";
-		local stamp = datetime.datetime();
-		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203
-		stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
-		local entry = { stanza = stanza, stamp = stamp };
-		t_insert(history, entry);
-		while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
+		return self:save_to_history(stanza)
 	end
 end
+function room_mt:save_to_history(stanza)
+	local history = self._data['history'];
+	if not history then history = {}; self._data['history'] = history; end
+	stanza = st.clone(stanza);
+	stanza.attr.to = "";
+	local stamp = datetime.datetime();
+	stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203
+	stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
+	local entry = { stanza = stanza, stamp = stamp };
+	t_insert(history, entry);
+	while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
+end
 function room_mt:broadcast_except_nick(stanza, nick)
 	for rnick, occupant in pairs(self._occupants) do
 		if rnick ~= nick then
@@ -170,10 +150,10 @@
 	if history then
 		local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
 		local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
-		
+
 		local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
 		if maxchars then maxchars = math.floor(maxchars); end
-		
+
 		local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
 		if not history_tag then maxstanzas = 20; end
 
@@ -186,7 +166,7 @@
 
 		local n = 0;
 		local charcount = 0;
-		
+
 		for i=#history,1,-1 do
 			local entry = history[i];
 			if maxchars then
@@ -207,6 +187,8 @@
 			self:_route_stanza(msg);
 		end
 	end
+end
+function room_mt:send_subject(to)
 	if self._data['subject'] then
 		self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
 	end
@@ -218,10 +200,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 +220,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 +277,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 +287,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 +297,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 +307,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,12 +338,25 @@
 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];
 	local occupant = room._occupants[to_nick];
 	local to_jid = occupant.jid;
-	
+
 	return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid));
 end
 local function deconstruct_stanza_id(room, stanza)
@@ -485,6 +485,12 @@
 					log("debug", "%s joining as %s", from, to);
 					if not next(self._affiliations) then -- new room, no owners
 						self._affiliations[jid_bare(from)] = "owner";
+						if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then
+							self.locked = nil; -- Older groupchat protocol doesn't lock
+						end
+					elseif self.locked then -- Deny entry
+						origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+						return;
 					end
 					local affiliation = self:get_affiliation(from);
 					local role = self:get_default_role(affiliation)
@@ -506,9 +512,13 @@
 						if self._data.whois == 'anyone' then
 							pr:tag("status", {code='100'}):up();
 						end
+						if self.locked then
+							pr:tag("status", {code='201'}):up();
+						end
 						pr.attr.to = from;
 						self:_route_stanza(pr);
 						self:send_history(from, stanza);
+						self:send_subject(from);
 					elseif not affiliation then -- registration required for entering members-only room
 						local reply = st.error_reply(stanza, "auth", "registration-required"):up();
 						reply.tags[1].attr.code = "407";
@@ -560,6 +570,7 @@
 				end
 				stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
 			else -- message
+				stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
 				stanza.attr.from = current_nick;
 				for jid in pairs(o_data.sessions) do
 					stanza.attr.to = jid;
@@ -575,11 +586,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 +615,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 +648,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 +663,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 +674,50 @@
 	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
+	if self.locked then
+		module:fire_event("muc-room-unlocked", { room = self });
+		self.locked = nil;
+	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 +853,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 +863,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 +915,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
--- a/plugins/storage/sqlbasic.lib.lua	Tue Feb 24 15:19:20 2015 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-
--- Basic SQL driver
--- This driver stores data as simple key-values
-
-local ser = require "util.serialization".serialize;
-local envload = require "util.envload".envload;
-local deser = function(data)
-	module:log("debug", "deser: %s", tostring(data));
-	if not data then return nil; end
-	local f = envload("return "..data, nil, {});
-	if not f then return nil; end
-	local s, d = pcall(f);
-	if not s then return nil; end
-	return d;
-end;
-
-local driver = {};
-driver.__index = driver;
-
-driver.item_table = "item";
-driver.list_table = "list";
-
-function driver:prepare(sql)
-	module:log("debug", "query: %s", sql);
-	local err;
-	if not self.sqlcache then self.sqlcache = {}; end
-	local r = self.sqlcache[sql];
-	if r then return r; end
-	r, err = self.connection:prepare(sql);
-	if not r then error("Unable to prepare SQL statement: "..err); end
-	self.sqlcache[sql] = r;
-	return r;
-end
-
-function driver:load(username, host, datastore)
-	local select = self:prepare("select data from "..self.item_table.." where username=? and host=? and datastore=?");
-	select:execute(username, host, datastore);
-	local row = select:fetch();
-	return row and deser(row[1]) or nil;
-end
-
-function driver:store(username, host, datastore, data)
-	if not data or next(data) == nil then
-		local delete = self:prepare("delete from "..self.item_table.." where username=? and host=? and datastore=?");
-		delete:execute(username, host, datastore);
-		return true;
-	else
-		local d = self:load(username, host, datastore);
-		if d then -- update
-			local update = self:prepare("update "..self.item_table.." set data=? where username=? and host=? and datastore=?");
-			return update:execute(ser(data), username, host, datastore);
-		else -- insert
-			local insert = self:prepare("insert into "..self.item_table.." values (?, ?, ?, ?)");
-			return insert:execute(username, host, datastore, ser(data));
-		end
-	end
-end
-
-function driver:list_append(username, host, datastore, data)
-	if not data then return; end
-	local insert = self:prepare("insert into "..self.list_table.." values (?, ?, ?, ?)");
-	return insert:execute(username, host, datastore, ser(data));
-end
-
-function driver:list_store(username, host, datastore, data)
-	-- remove existing data
-	local delete = self:prepare("delete from "..self.list_table.." where username=? and host=? and datastore=?");
-	delete:execute(username, host, datastore);
-	if data and next(data) ~= nil then
-		-- add data
-		for _, d in ipairs(data) do
-			self:list_append(username, host, datastore, ser(d));
-		end
-	end
-	return true;
-end
-
-function driver:list_load(username, host, datastore)
-	local select = self:prepare("select data from "..self.list_table.." where username=? and host=? and datastore=?");
-	select:execute(username, host, datastore);
-	local r = {};
-	for row in select:rows() do
-		table.insert(r, deser(row[1]));
-	end
-	return r;
-end
-
-local _M = {};
-function _M.new(dbtype, dbname, ...)
-	local d = {};
-	setmetatable(d, driver);
-	local dbh = get_database(dbtype, dbname, ...);
-	--d:set_connection(dbh);
-	d.connection = dbh;
-	return d;
-end
-return _M;
--- a/prosody	Tue Feb 24 15:19:20 2015 +0000
+++ b/prosody	Tue Feb 24 15:22:09 2015 +0000
@@ -151,9 +151,12 @@
 	-- for neat sandboxing of modules
 	local _realG = _G;
 	local _real_require = require;
-	if not getfenv then
+	local getfenv = getfenv or function (f)
 		-- FIXME: This is a hack to replace getfenv() in Lua 5.2
-		function getfenv(f) return debug.getupvalue(debug.getinfo(f or 1).func, 1); end
+		local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1);
+		if name == "_ENV" then
+			return env;
+		end
 	end
 	function require(...)
 		local curr_env = getfenv(2);
@@ -268,12 +271,6 @@
 		prosody.events.fire_event("server-stopping", {reason = reason});
 		server.setquitting(true);
 	end
-
-	-- Load SSL settings from config, and create a ctx table
-	local certmanager = require "core.certmanager";
-	local global_ssl_ctx = certmanager.create_context("*", "server");
-	prosody.global_ssl_ctx = global_ssl_ctx;
-
 end
 
 function read_version()
@@ -295,6 +292,7 @@
 	require "util.import"
 	require "util.xmppstream"
 	require "core.stanza_router"
+	require "core.statsmanager"
 	require "core.hostmanager"
 	require "core.portmanager"
 	require "core.modulemanager"
--- a/prosody.cfg.lua.dist	Tue Feb 24 15:19:20 2015 +0000
+++ b/prosody.cfg.lua.dist	Tue Feb 24 15:22:09 2015 +0000
@@ -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.
 --
@@ -24,7 +24,7 @@
 
 -- Enable use of libevent for better performance under high load
 -- For more information see: http://prosody.im/doc/libevent
---use_libevent = true;
+--use_libevent = true
 
 -- This is the list of modules Prosody will load on startup.
 -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
@@ -43,7 +43,7 @@
 		"vcard"; -- Allow users to set vCards
 	
 	-- These are commented by default as they have a performance impact
-		--"privacy"; -- Support privacy lists
+		--"blocklist"; -- Allow users to block communications with other users
 		--"compression"; -- Stream compression
 
 	-- Nice to have
@@ -63,14 +63,13 @@
 		--"http_files"; -- Serve static files from a directory over HTTP
 
 	-- Other specific functionality
-		--"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
 		--"groups"; -- Shared roster support
 		--"announce"; -- Send announcement to all online users
 		--"welcome"; -- Welcome users who register accounts
 		--"watchregistrations"; -- Alert admins of registrations
 		--"motd"; -- Send a message to users when they log in
 		--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-};
+}
 
 -- These modules are auto-loaded, but should you want
 -- to disable them then uncomment them here:
@@ -78,11 +77,12 @@
 	-- "offline"; -- Store offline messages
 	-- "c2s"; -- Handle client connections
 	-- "s2s"; -- Handle server-to-server connections
-};
+	-- "posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
+}
 
 -- Disable account creation by default, for security
 -- For more information see http://prosody.im/doc/creating_accounts
-allow_registration = false;
+allow_registration = false
 
 -- These are the SSL/TLS-related settings. If you don't want
 -- to use SSL/TLS, you may comment or remove this
@@ -94,7 +94,7 @@
 -- Force clients to use encrypted connections? This option will
 -- prevent clients from authenticating unless they are using encryption.
 
-c2s_require_encryption = false
+c2s_require_encryption = true
 
 -- Force certificate authentication for server-to-server connections?
 -- This provides ideal security, but requires servers you communicate
--- a/prosodyctl	Tue Feb 24 15:19:20 2015 +0000
+++ b/prosodyctl	Tue Feb 24 15:22:09 2015 +0000
@@ -320,7 +320,7 @@
 		show_usage([[passwd JID]], [[Set the password for the specified user account in Prosody]]);
 		return 1;
 	end
-	local user, host = jid_split(arg[1])
+	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]]
@@ -413,7 +413,11 @@
 	
 	local ok, ret = prosodyctl.start();
 	if ok then
-		if config.get("*", "daemonize") ~= false then
+		local daemonize = config.get("*", "daemonize");
+		if daemonize == nil then
+			daemonize = prosody.installed;
+		end
+		if daemonize then
 			local i=1;
 			while true do
 				local ok, running = prosodyctl.isrunning();
@@ -732,7 +736,7 @@
 		end
 		local _, key_filename = cert_commands.key({arg[1]});
 		local _, conf_filename = cert_commands.config(arg);
-		if openssl.req{new=true, key=key_filename, utf8=true, config=conf_filename, out=req_filename} then
+		if openssl.req{new=true, key=key_filename, utf8=true, sha256=true, config=conf_filename, out=req_filename} then
 			show_message("Certificate request written to ".. req_filename);
 		else
 			show_message("There was a problem, see OpenSSL output");
@@ -753,7 +757,7 @@
 		local ret;
 		if key_filename and conf_filename and cert_filename
 			and openssl.req{new=true, x509=true, nodes=true, key=key_filename,
-				days=365, sha1=true, utf8=true, config=conf_filename, out=cert_filename} then
+				days=365, sha256=true, utf8=true, config=conf_filename, out=cert_filename} then
 			show_message("Certificate written to ".. cert_filename);
 		else
 			show_message("There was a problem, see OpenSSL output");
@@ -783,6 +787,369 @@
 	show_usage("cert config|request|generate|key", "Helpers for generating X.509 certificates and keys.")
 end
 
+function commands.check(arg)
+	if arg[1] == "--help" then
+		show_usage([[check]], [[Perform basic checks on your Prosody installation]]);
+		return 1;
+	end
+	local what = table.remove(arg, 1);
+	local array, set = require "util.array", require "util.set";
+	local it = require "util.iterators";
+	local ok = true;
+	local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end
+	local function enabled_hosts() return it.filter(disabled_hosts, pairs(config.getconfig())); end
+	if not what or what == "disabled" then
+		local disabled_hosts = set.new();
+		for host, host_options in it.filter("*", pairs(config.getconfig())) do
+			if host_options.enabled == false then
+				disabled_hosts:add(host);
+			end
+		end
+		if not disabled_hosts:empty() then
+			local msg = "Checks will be skipped for these disabled hosts: %s";
+			if what then msg = "These hosts are disabled: %s"; end
+			show_warning(msg, tostring(disabled_hosts));
+			if what then return 0; end
+			print""
+		end
+	end
+	if not what or what == "config" then
+		print("Checking config...");
+		local deprecated = set.new({
+			"bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption",
+		});
+		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
+		if it.count(enabled_hosts()) == 0 then
+			ok = false;
+			print("");
+			if it.count(it.filter("*", pairs(config))) == 0 then
+				print("    No hosts are defined, please add at least one VirtualHost section")
+			elseif config["*"]["enabled"] == false then
+				print("    No hosts are enabled. Remove enabled = false from the global section or put enabled = true under at least one VirtualHost section")
+			else
+				print("    All hosts are disabled. Remove enabled = false from at least one VirtualHost section")
+			end
+		end
+		-- Check for global options under hosts
+		local global_options = set.new(it.to_array(it.keys(config["*"])));
+		local deprecated_global_options = set.intersection(global_options, deprecated);
+		if not deprecated_global_options:empty() then
+			print("");
+			print("    You have some deprecated options in the global section:");
+			print("    "..tostring(deprecated_global_options))
+			ok = false;
+		end
+		for host, options in enabled_hosts() 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 idna = require "util.encodings".idna;
+		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(idna.to_ascii(fqdn), "A");
+			if res then
+				for _, record in ipairs(res) do
+					external_addresses:add(record.a);
+				end
+			end
+			local res = dns.lookup(idna.to_ascii(fqdn), "AAAA");
+			if res then
+				for _, record in ipairs(res) do
+					external_addresses:add(record.aaaa);
+				end
+			end
+		end
+		
+		local local_addresses = require"util.net".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 enabled_hosts() 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."..idna.to_ascii(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."..idna.to_ascii(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(idna.to_ascii(proxy65_target), "A"), dns.lookup(idna.to_ascii(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(idna.to_ascii(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(idna.to_ascii(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 what or what == "certs" then
+		local cert_ok;
+		print"Checking certificates..."
+		local x509_verify_identity = require"util.x509".verify_identity;
+		local ssl = dependencies.softreq"ssl";
+		-- local datetime_parse = require"util.datetime".parse_x509;
+		local load_cert = ssl and ssl.x509 and ssl.x509.load;
+		-- or ssl.cert_from_pem
+		if not ssl then
+			print("LuaSec not available, can't perform certificate checks")
+			if what == "certs" then cert_ok = false end
+		elseif not load_cert then
+			print("This version of LuaSec (" .. ssl._VERSION .. ") does not support certificate checking");
+			cert_ok = false
+		else
+			for host in enabled_hosts() do
+				print("Checking certificate for "..host);
+				-- First, let's find out what certificate this host uses.
+				local ssl_config = config.rawget(host, "ssl");
+				if not ssl_config then
+					local base_host = host:match("%.(.*)");
+					ssl_config = config.get(base_host, "ssl");
+				end
+				if not ssl_config then
+					print("  No 'ssl' option defined for "..host)
+					cert_ok = false
+				elseif not ssl_config.certificate then
+					print("  No 'certificate' set in ssl option for "..host)
+					cert_ok = false
+				elseif not ssl_config.key then
+					print("  No 'key' set in ssl option for "..host)
+					cert_ok = false
+				else
+					local key, err = io.open(ssl_config.key); -- Permissions check only
+					if not key then
+						print("    Could not open "..ssl_config.key..": "..err);
+						cert_ok = false
+					else
+						key:close();
+					end
+					local cert_fh, err = io.open(ssl_config.certificate); -- Load the file.
+					if not cert_fh then
+						print("    Could not open "..ssl_config.certificate..": "..err);
+						cert_ok = false
+					else
+						print("  Certificate: "..ssl_config.certificate)
+						local cert = load_cert(cert_fh:read"*a"); cert_fh = cert_fh:close();
+						if not cert:validat(os.time()) then
+							print("    Certificate has expired.")
+							cert_ok = false
+						end
+						if config.get(host, "component_module") == nil
+							and not x509_verify_identity(host, "_xmpp-client", cert) then
+							print("    Not vaild for client connections to "..host..".")
+							cert_ok = false
+						end
+						if (not (config.get(host, "anonymous_login")
+							or config.get(host, "authentication") == "anonymous"))
+							and not x509_verify_identity(host, "_xmpp-client", cert) then
+							print("    Not vaild for server-to-server connections to "..host..".")
+							cert_ok = false
+						end
+					end
+				end
+			end
+			if cert_ok == false then
+				print("")
+				print("For more information about certificates please see http://prosody.im/doc/certificates");
+				ok = false
+			end
+		end
+		print("")
+	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/modulemanager_option_conversion.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/modulemanager_option_conversion.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -18,7 +18,7 @@
 	assert(module:get_option_number("opt") == returns.number, "number doesn't match");
 	assert(module:get_option_string("opt") == returns.string, "string doesn't match");
 	assert(module:get_option_boolean("opt") == returns.boolean, "boolean doesn't match");
-	
+
 	if type(returns.array) == "table" then
 		local target_array, returned_array = returns.array, module:get_option_array("opt");
 		assert(#target_array == #returned_array, "array length doesn't match");
@@ -28,7 +28,7 @@
 	else
 		assert(module:get_option_array("opt") == returns.array, "array is returned (not nil)");
 	end
-	
+
 	if type(returns.set) == "table" then
 		local target_items, returned_items = set.new(returns.set), module:get_option_set("opt");
 		assert(target_items == returned_items, "set doesn't match");
--- a/tests/test.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -12,15 +12,15 @@
 	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"
-	
+
 	dosingletest("test_sasl.lua", "latin1toutf8");
 end
 
@@ -87,12 +87,12 @@
 		print("WARNING: ", "Failed to initialise tests for "..testname, err);
 		return;
 	end
-	
+
 	if type(tests[fname]) ~= "function" then
 		error(testname.." has no test '"..fname.."'", 0);
 	end
-	
-	
+
+
 	local line_hook, line_info = new_line_coverage_monitor(testname);
 	debug.sethook(line_hook, "l")
 	local success, ret = pcall(tests[fname]);
@@ -134,17 +134,23 @@
 		print("WARNING: ", "Failed to load module: "..unitname, err);
 		return;
 	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
@@ -191,11 +197,11 @@
 function new_line_coverage_monitor(file)
 	local lines_hit, funcs_hit = {}, {};
 	local total_lines, covered_lines = 0, 0;
-	
+
 	for line in io.lines(file) do
 		total_lines = total_lines + 1;
 	end
-	
+
 	return function (event, line) -- Line hook
 			if not lines_hit[line] then
 				local info = debug.getinfo(2, "fSL")
--- a/tests/test_core_configmanager.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test_core_configmanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -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("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("*", "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", ""); -- 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	Tue Feb 24 15:19:20 2015 +0000
+++ /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	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test_core_s2smanager.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,11 +1,14 @@
 -- 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.
 --
 
+env = {
+	prosody = { events = require "util.events".new() };
+};
 
 function compare_srv_priorities(csp)
 	local r1 = { priority = 10, weight = 0 }
@@ -13,7 +16,7 @@
 	local r3 = { priority = 1000, weight = 2 }
 	local r4 = { priority = 1000, weight = 2 }
 	local r5 = { priority = 1000, weight = 5 }
-	
+
 	assert_equal(csp(r1, r1), false);
 	assert_equal(csp(r1, r2), true);
 	assert_equal(csp(r1, r3), true);
--- a/tests/test_core_stanza_router.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test_core_stanza_router.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -14,7 +14,7 @@
 	local s2sin_session = { from_host = "remotehost", to_host = "localhost", type = "s2sin", hosts = { ["remotehost"] = { authed = true } } }
 	local local_host_session = { host = "localhost", type = "local", s2sout = { ["remotehost"] = s2sout_session } }
 	local local_user_session = { username = "user", host = "localhost", resource = "resource", full_jid = "user@localhost/resource", type = "c2s" }
-	
+
 	_G.prosody.hosts["localhost"] = local_host_session;
 	_G.prosody.full_sessions["user@localhost/resource"] = local_user_session;
 	_G.prosody.bare_sessions["user@localhost"] = { sessions = { resource = local_user_session } };
@@ -23,15 +23,15 @@
 	local function test_message_full_jid()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "user@localhost/resource", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct");
 			assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print());
 			target_routed = true;
 		end
-		
+
 		env.hosts = hosts;
 		env.prosody = { hosts = hosts };
 		setfenv(core_process_stanza, env);
@@ -42,9 +42,9 @@
 	local function test_message_bare_jid()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "user@localhost", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of routed stanza is not correct");
 			assert_equal(p_stanza, msg, "routed stanza is not correct one: "..p_stanza:pretty_print());
@@ -60,9 +60,9 @@
 	local function test_message_no_to()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_handled;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -78,9 +78,9 @@
 	local function test_message_to_remote_bare()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "user@remotehost", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -88,7 +88,7 @@
 		end
 
 		function env.core_post_stanza(...) env.core_route_stanza(...); end
-		
+
 		env.hosts = hosts;
 		setfenv(core_process_stanza, env);
 		assert_equal(core_process_stanza(local_user_session, msg), nil, "core_process_stanza returned incorrect value");
@@ -98,9 +98,9 @@
 	local function test_message_to_remote_server()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("message", { to = "remotehost", type = "chat" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -123,9 +123,9 @@
 	local function test_iq_to_remote_server()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("iq", { to = "remotehost", type = "get", id = "id" }):tag("body"):text("Hello world");
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -145,9 +145,9 @@
 	local function test_iq_error_to_local_user()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("iq", { to = "user@localhost/resource", from = "user@remotehost", type = "error", id = "id" }):tag("error", { type = 'cancel' }):tag("item-not-found", { xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' });
-		
+
 		local target_routed;
-		
+
 		function env.core_route_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, s2sin_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -167,9 +167,9 @@
 	local function test_iq_to_local_bare()
 		local env = testlib_new_env();
 		local msg = stanza.stanza("iq", { to = "user@localhost", from = "user@localhost", type = "get", id = "id" }):tag("ping", { xmlns = "urn:xmpp:ping:0" });
-		
+
 		local target_handled;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			assert_equal(p_origin, local_user_session, "origin of handled stanza is not correct");
 			assert_equal(p_stanza, msg, "handled stanza is not correct one: "..p_stanza:pretty_print());
@@ -209,11 +209,11 @@
 		local msg2 = stanza.stanza("iq", { to = "user@localhost/foo", from = "user@localhost", type = "error" }):tag("ping", { xmlns = "urn:xmpp:ping:0" });
 		--package.loaded["core.usermanager"] = { user_exists = function (user, host) print("RAR!") return true or user == "user" and host == "localhost" and true; end };
 		local target_handled, target_replied;
-		
+
 		function env.core_post_stanza(p_origin, p_stanza)
 			target_handled = true;
 		end
-		
+
 		function local_user_session.send(data)
 			--print("Replying with: ", tostring(data));
 			--print(debug.traceback())
--- a/tests/test_net_http.lua	Tue Feb 24 15:19:20 2015 +0000
+++ /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
--- a/tests/test_sasl.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test_sasl.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -30,7 +30,7 @@
 	local function assert_utf8(latin, utf8)
 			assert_equal(_latin1toutf8(latin), utf8, "Incorrect UTF8 from Latin1: "..tostring(latin));
 	end
-	
+
 	assert_utf8("", "")
 	assert_utf8("test", "test")
 	assert_utf8(nil, nil)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_util_http.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -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	Tue Feb 24 15:22:09 2015 +0000
@@ -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_jid.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test_util_jid.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/tests/test_util_multitable.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test_util_multitable.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -41,7 +41,7 @@
 	end
 
 	mt = multitable.new();
-	
+
 	local trigger1, trigger2, trigger3 = {}, {}, {};
 	local item1, item2, item3 = {}, {}, {};
 
@@ -51,12 +51,12 @@
 	mt:add(1, 2, 3, item1);
 
 	assert_has_all("Has item1 for 1, 2, 3", mt:get(1, 2, 3), item1);
-	
+
 -- Doesn't support nil
 --[[	mt:add(nil, item1);
 	mt:add(nil, item2);
 	mt:add(nil, item3);
-	
+
 	assert_has_all("Has all items with (nil)", mt:get(nil), item1, item2, item3);
 ]]
 end
--- a/tests/test_util_rfc3484.lua	Tue Feb 24 15:19:20 2015 +0000
+++ /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	Tue Feb 24 15:22:09 2015 +0000
@@ -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/tests/test_util_stanza.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/test_util_stanza.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -18,7 +18,7 @@
 
 function deserialize(deserialize, st)
 	local stanza = st.stanza("message", { a = "a" });
-	
+
 	local stanza2 = deserialize(st.preserialize(stanza));
 	assert_is(stanza2 and stanza.name, "deserialize returns a stanza");
 	assert_table(stanza2.attr, "Deserialized stanza has attributes");
--- a/tests/util/logger.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tests/util/logger.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/tools/ejabberd2prosody.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/ejabberd2prosody.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -2,7 +2,7 @@
 -- 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.
 --
--- a/tools/ejabberdsql2prosody.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/ejabberdsql2prosody.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -2,7 +2,7 @@
 -- 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.
 --
--- a/tools/erlparse.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/erlparse.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/tools/jabberd14sql2prosody.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/jabberd14sql2prosody.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -5,242 +5,242 @@
 
 
 local _parse_sql_actions = { [0] =
-  0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13, 
-  2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0, 
+  0, 1, 0, 1, 1, 2, 0, 2, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 13,
+  2, 1, 2, 2, 1, 6, 3, 0, 3, 4, 3, 0, 3, 5, 3, 0, 3, 7, 3, 0,
   3, 8, 3, 0, 3, 12, 4, 0, 2, 3, 7, 4, 0, 3, 8, 11
 };
 
 local _parse_sql_trans_keys = { [0] =
-  0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82, 
-  69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65, 
-  65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69, 
-  9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 
-  10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42, 
-  42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69, 
-  32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84, 
-  32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 
-  83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40, 
-  10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32, 
-  96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77, 
-  77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69, 
-  89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69, 
-  32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10, 
-  59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65, 
-  66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32, 
-  69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32, 
-  32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83, 
-  69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84, 
-  79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 
-  86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85, 
-  69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92, 
-  41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41, 
-  57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67, 
-  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69, 
-  83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87, 
-  87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84, 
-  32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67, 
-  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 
+  0, 0, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47, 82, 82,
+  69, 69, 65, 65, 84, 84, 69, 69, 32, 32, 68, 84, 65,
+  65, 84, 84, 65, 65, 66, 66, 65, 65, 83, 83, 69, 69,
+  9, 47, 9, 96, 45, 45, 10, 10, 42, 42, 10, 42, 10, 47,
+  10, 96, 10, 96, 9, 47, 9, 59, 45, 45, 10, 10, 42,
+  42, 10, 42, 10, 47, 65, 65, 66, 66, 76, 76, 69, 69,
+  32, 32, 73, 96, 70, 70, 32, 32, 78, 78, 79, 79, 84, 84,
+  32, 32, 69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83,
+  83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40, 40,
+  10, 10, 32, 41, 32, 32, 75, 96, 69, 69, 89, 89, 32, 32,
+  96, 96, 10, 96, 10, 96, 10, 10, 82, 82, 73, 73, 77,
+  77, 65, 65, 82, 82, 89, 89, 32, 32, 75, 75, 69, 69,
+  89, 89, 32, 32, 78, 78, 73, 73, 81, 81, 85, 85, 69, 69,
+  32, 32, 75, 75, 10, 96, 10, 96, 10, 10, 10, 59, 10,
+  59, 82, 82, 79, 79, 80, 80, 32, 32, 84, 84, 65, 65,
+  66, 66, 76, 76, 69, 69, 32, 32, 73, 73, 70, 70, 32, 32,
+  69, 69, 88, 88, 73, 73, 83, 83, 84, 84, 83, 83, 32,
+  32, 96, 96, 10, 96, 10, 96, 59, 59, 78, 78, 83, 83,
+  69, 69, 82, 82, 84, 84, 32, 32, 73, 73, 78, 78, 84, 84,
+  79, 79, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 40,
+  86, 10, 41, 32, 32, 86, 86, 65, 65, 76, 76, 85, 85,
+  69, 69, 83, 83, 32, 32, 40, 40, 39, 78, 10, 92, 10, 92,
+  41, 44, 44, 59, 32, 78, 48, 57, 41, 57, 48, 57, 41,
+  57, 85, 85, 76, 76, 76, 76, 34, 116, 79, 79, 67, 67,
+  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69, 69,
+  83, 83, 32, 32, 96, 96, 10, 96, 10, 96, 32, 32, 87,
+  87, 82, 82, 73, 73, 84, 84, 69, 69, 69, 69, 84, 84,
+  32, 32, 10, 59, 10, 59, 78, 83, 76, 76, 79, 79, 67, 67,
+  75, 75, 32, 32, 84, 84, 65, 65, 66, 66, 76, 76, 69,
   69, 83, 83, 69, 69, 9, 85, 0
 };
 
 local _parse_sql_key_spans = { [0] =
-  0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1, 
-  39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1, 
-  1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 
-  1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 
+  0, 1, 1, 1, 33, 38, 1, 1, 1, 1, 1, 1, 17, 1, 1, 1, 1, 1, 1, 1,
+  39, 88, 1, 1, 1, 33, 38, 87, 87, 39, 51, 1, 1, 1, 33, 38, 1, 1, 1, 1,
+  1, 24, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1,
+  1, 10, 1, 22, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 50, 50, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 47, 32, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 40, 83, 83, 4, 16, 47, 10, 17, 10, 17, 1, 1, 1, 83, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 87, 87, 1, 1, 1, 1, 1, 1, 1, 1,
   1, 50, 50, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 77
 };
 
 local _parse_sql_index_offsets = { [0] =
-  0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121, 
-  123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684, 
-  686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919, 
-  921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161, 
-  1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471, 
-  1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683, 
-  1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972, 
-  1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411, 
-  2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623, 
+  0, 0, 2, 4, 6, 40, 79, 81, 83, 85, 87, 89, 91, 109, 111, 113, 115, 117, 119, 121,
+  123, 163, 252, 254, 256, 258, 292, 331, 419, 507, 547, 599, 601, 603, 605, 639, 678, 680, 682, 684,
+  686, 688, 713, 715, 717, 719, 721, 723, 725, 727, 729, 731, 733, 735, 737, 739, 741, 829, 917, 919,
+  921, 923, 934, 936, 959, 961, 963, 965, 967, 1055, 1143, 1145, 1147, 1149, 1151, 1153, 1155, 1157, 1159, 1161,
+  1163, 1165, 1167, 1169, 1171, 1173, 1175, 1177, 1179, 1181, 1269, 1357, 1359, 1410, 1461, 1463, 1465, 1467, 1469, 1471,
+  1473, 1475, 1477, 1479, 1481, 1483, 1485, 1487, 1489, 1491, 1493, 1495, 1497, 1499, 1501, 1503, 1591, 1679, 1681, 1683,
+  1685, 1687, 1689, 1691, 1693, 1695, 1697, 1699, 1701, 1703, 1705, 1793, 1881, 1883, 1931, 1964, 1966, 1968, 1970, 1972,
+  1974, 1976, 1978, 1980, 1982, 2023, 2107, 2191, 2196, 2213, 2261, 2272, 2290, 2301, 2319, 2321, 2323, 2325, 2409, 2411,
+  2413, 2415, 2417, 2419, 2421, 2423, 2425, 2427, 2429, 2431, 2433, 2521, 2609, 2611, 2613, 2615, 2617, 2619, 2621, 2623,
   2625, 2627, 2678, 2729, 2736, 2738, 2740, 2742, 2744, 2746, 2748, 2750, 2752, 2754, 2756, 2758, 2760
 };
 
 local _parse_sql_indicies = { [0] =
-  0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 
-  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 
-  4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 
-  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7, 
-  1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20, 
-  1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, 
-  1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24, 
-  1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28, 
-  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 
-  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28, 
-  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 
-  28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 
-  31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38, 
-  1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 
-  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40, 
-  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 
-  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1, 
-  43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52, 
-  1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48, 
-  1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 
-  62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 
-  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68, 
-  1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1, 
-  1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77, 
-  1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 
-  81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91, 
-  1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73, 
-  1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 
-  100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 
-  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 
-  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107, 
-  71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118, 
-  1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128, 
-  1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 
-  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6, 
-  1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142, 
-  1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 
-  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 
-  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149, 
-  147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151, 
-  151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 
-  151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1, 
-  162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 
-  165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 
-  167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 
-  171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1, 
-  163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166, 
-  1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179, 
-  179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 
-  1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184, 
-  1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192, 
-  1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1, 
-  199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 
-  199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132, 
-  1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 
-  209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1, 
-  216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1, 
-  6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227, 
+  0, 1, 2, 0, 3, 1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3,
+  4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+  3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 3, 3, 3, 3, 6, 3, 7,
+  1, 8, 1, 9, 1, 10, 1, 11, 1, 12, 1, 13, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 14, 1, 15, 1, 16, 1, 17, 1, 18, 1, 19, 1, 20,
+  1, 21, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24,
+  1, 25, 1, 22, 23, 22, 22, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 22, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 24,
+  1, 25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 26, 1, 27, 1, 23, 27, 28, 1, 29, 28,
+  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 29, 28, 28, 28, 28, 28, 28, 28,
+  28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+  28, 28, 28, 28, 30, 28, 28, 28, 28, 22, 28, 32, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 1, 31, 32,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31,
+  31, 31, 31, 31, 31, 33, 31, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 36, 1, 37, 1, 34, 35, 34, 34, 34, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 34, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 36, 1, 37, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 38,
+  1, 35, 38, 39, 1, 40, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
+  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 40,
+  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39,
+  39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 39, 41, 39, 39, 39, 39, 34, 39, 42, 1,
+  43, 1, 44, 1, 45, 1, 46, 1, 47, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 48, 1, 49, 1, 50, 1, 51, 1, 52,
+  1, 53, 1, 54, 1, 55, 1, 56, 1, 57, 1, 58, 1, 59, 1, 60, 1, 61, 1, 48,
+  1, 63, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62,
+  62, 62, 62, 62, 62, 62, 62, 1, 62, 65, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+  64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 66, 64, 67, 1, 68,
+  1, 69, 1, 70, 1, 1, 1, 1, 1, 1, 1, 1, 71, 1, 72, 1, 73, 1, 1, 1,
+  1, 74, 1, 1, 1, 1, 75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 76, 1, 77,
+  1, 78, 1, 79, 1, 80, 1, 82, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 1, 81, 82, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
+  81, 83, 81, 69, 83, 84, 1, 85, 1, 86, 1, 87, 1, 88, 1, 89, 1, 90, 1, 91,
+  1, 92, 1, 93, 1, 83, 1, 94, 1, 95, 1, 96, 1, 97, 1, 98, 1, 99, 1, 73,
+  1, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
+  100, 100, 100, 100, 100, 100, 100, 1, 100, 103, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
+  102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 104, 102, 105, 83, 106,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 107, 71, 108, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71,
+  71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 107,
+  71, 109, 1, 110, 1, 111, 1, 112, 1, 113, 1, 114, 1, 115, 1, 116, 1, 117, 1, 118,
+  1, 119, 1, 120, 1, 121, 1, 122, 1, 123, 1, 124, 1, 125, 1, 126, 1, 127, 1, 128,
+  1, 129, 1, 131, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 1, 130, 131, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130,
+  130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 130, 132, 130, 6,
+  1, 133, 1, 134, 1, 135, 1, 136, 1, 137, 1, 138, 1, 139, 1, 140, 1, 141, 1, 142,
+  1, 143, 1, 144, 1, 146, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145,
+  145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 145, 1, 145, 148, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147,
+  147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 149,
+  147, 150, 1, 151, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 152, 1, 153, 151, 151, 151, 151, 151, 151, 151, 151,
+  151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151, 151,
+  151, 151, 154, 151, 155, 1, 152, 1, 156, 1, 157, 1, 158, 1, 159, 1, 160, 1, 161, 1,
+  162, 1, 163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165,
+  165, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 166, 1, 168, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 169, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167, 167,
+  167, 167, 167, 167, 167, 170, 167, 172, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 173, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171, 171,
+  171, 171, 171, 171, 171, 171, 171, 171, 171, 174, 171, 175, 1, 1, 176, 1, 161, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 177, 1, 178, 1, 1, 1, 1, 1, 1,
+  163, 1, 1, 1, 1, 1, 164, 1, 1, 165, 165, 165, 165, 165, 165, 165, 165, 165, 165, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 166,
+  1, 179, 179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 180, 1, 1, 181, 1, 182, 1, 179,
+  179, 179, 179, 179, 179, 179, 179, 179, 179, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183,
+  1, 180, 1, 1, 181, 1, 1, 1, 183, 183, 183, 183, 183, 183, 183, 183, 183, 183, 1, 184,
+  1, 185, 1, 186, 1, 171, 1, 1, 171, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 171,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 171, 1, 171, 1, 1, 171, 1, 1, 171, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 171, 1, 1, 1, 171, 1, 171, 1, 187, 1, 188, 1, 189, 1, 190, 1, 191, 1, 192,
+  1, 193, 1, 194, 1, 195, 1, 196, 1, 197, 1, 198, 1, 200, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 1,
+  199, 200, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199, 199,
+  199, 199, 199, 199, 199, 199, 199, 201, 199, 202, 1, 203, 1, 204, 1, 205, 1, 206, 1, 132,
+  1, 207, 1, 208, 1, 209, 1, 210, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 2, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209,
+  209, 209, 209, 209, 209, 209, 209, 211, 209, 212, 1, 1, 1, 1, 213, 1, 214, 1, 215, 1,
+  216, 1, 217, 1, 218, 1, 219, 1, 220, 1, 221, 1, 222, 1, 223, 1, 132, 1, 127, 1,
+  6, 2, 6, 6, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 224, 1, 225, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 226, 227,
   1, 1, 1, 1, 228, 1, 1, 229, 1, 1, 1, 1, 1, 1, 230, 1, 231, 1, 0
 };
 
 local _parse_sql_trans_targs = { [0] =
-  2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18, 
-  19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34, 
-  34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 
-  54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67, 
-  68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 
-  90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 
-  106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125, 
-  126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141, 
-  142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151, 
-  148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 
-  171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190, 
+  2, 0, 196, 4, 4, 5, 196, 7, 8, 9, 10, 11, 12, 13, 36, 14, 15, 16, 17, 18,
+  19, 20, 21, 21, 22, 24, 27, 23, 25, 25, 26, 28, 28, 29, 30, 30, 31, 33, 32, 34,
+  34, 35, 37, 38, 39, 40, 41, 42, 56, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53,
+  54, 55, 57, 57, 57, 57, 58, 59, 60, 61, 62, 92, 63, 64, 71, 82, 89, 65, 66, 67,
+  68, 69, 69, 70, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88,
+  90, 90, 90, 90, 91, 70, 92, 93, 196, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
+  106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 116, 117, 119, 120, 121, 122, 123, 124, 125,
+  126, 127, 128, 129, 130, 131, 131, 131, 131, 132, 133, 134, 137, 134, 135, 136, 138, 139, 140, 141,
+  142, 143, 144, 145, 150, 151, 154, 146, 146, 147, 157, 146, 146, 147, 157, 148, 149, 196, 144, 151,
+  148, 149, 152, 153, 155, 156, 147, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171,
+  171, 172, 173, 174, 175, 176, 177, 179, 180, 181, 181, 182, 184, 195, 185, 186, 187, 188, 189, 190,
   191, 192, 193, 194, 1, 3, 6, 94, 118, 158, 178, 183
 };
 
 local _parse_sql_trans_actions = { [0] =
-  1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1, 
-  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 
-  1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 
-  1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1, 
-  51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
-  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 
+  1, 0, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 3, 1, 1, 1, 1, 1, 3, 1, 1, 3, 1, 1, 3, 1, 1, 1, 1,
+  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 5, 20, 1, 3, 30, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  5, 20, 1, 3, 26, 3, 3, 1, 23, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1,
+  1, 1, 1, 1, 1, 5, 20, 1, 3, 42, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1,
+  1, 1, 11, 1, 5, 5, 1, 5, 20, 46, 5, 1, 3, 34, 1, 14, 1, 17, 1, 1,
+  51, 38, 1, 1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+  3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1,
   1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
 };
 
@@ -277,7 +277,7 @@
 	local mark, token;
 	local table_name, columns, value_lists, value_list, value_count;
 
-	
+
   cs = parse_sql_start;
 
 --  ragel flat exec
@@ -322,10 +322,10 @@
       _inds = _parse_sql_index_offsets[cs];
       _slen = _parse_sql_key_spans[cs];
 
-      if   _slen > 0 and 
-         _parse_sql_trans_keys[_keys] <= data:byte(p) and 
-         data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then 
-        _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ]; 
+      if   _slen > 0 and
+         _parse_sql_trans_keys[_keys] <= data:byte(p) and
+         data:byte(p) <= _parse_sql_trans_keys[_keys + 1] then
+        _trans = _parse_sql_indicies[ _inds + data:byte(p) - _parse_sql_trans_keys[_keys] ];
       else _trans =_parse_sql_indicies[ _inds + _slen ]; end
 
     cs = _parse_sql_trans_targs[_trans];
@@ -364,7 +364,7 @@
        h.create(table_name, columns);       -- ACTION
         elseif _tempval  == 7 then --4 FROM_STATE_ACTION_SWITCH
 -- line 65 "sql.rl" -- end of line directive
-      
+
 			value_count = value_count + 1; value_list[value_count] = token:gsub("\\.", _sql_unescapes);
 		      -- ACTION
         elseif _tempval  == 8 then --4 FROM_STATE_ACTION_SWITCH
@@ -392,7 +392,7 @@
     end
 
     if _trigger_goto then _continue = true; break; end
-    end -- endif 
+    end -- endif
 
     if _goto_level <= _again then
       if cs == 0 then
--- a/tools/migration/migrator/prosody_sql.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/migration/migrator/prosody_sql.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -24,7 +24,7 @@
 	elseif params.driver == "MySQL" then
 		create_sql = create_sql:gsub("`value` TEXT", "`value` MEDIUMTEXT");
 	end
-	
+
 	local stmt = connection:prepare(create_sql);
 	if stmt then
 		local ok = stmt:execute();
--- a/tools/migration/prosody-migrator.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/migration/prosody-migrator.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -115,7 +115,7 @@
 	print("");
 	os.exit(1);
 end
-	
+
 local itype = config[from_store].type;
 local otype = config[to_store].type;
 local reader = require("migrator."..itype).reader(config[from_store]);
--- a/tools/openfire2prosody.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/openfire2prosody.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 #!/usr/bin/env lua
 -- Prosody IM
 -- Copyright (C) 2008-2009 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/tools/xep227toprosody.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/tools/xep227toprosody.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -3,7 +3,7 @@
 -- Copyright (C) 2008-2009 Matthew Wild
 -- Copyright (C) 2008-2009 Waqas Hussain
 -- Copyright (C) 2010      Stefan Gehn
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
--- a/util-src/encodings.c	Tue Feb 24 15:19:20 2015 +0000
+++ b/util-src/encodings.c	Tue Feb 24 15:22:09 2015 +0000
@@ -20,6 +20,10 @@
 #include "lua.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 502)
+#define luaL_register(L, N, R) luaL_setfuncs(L, R, 0)
+#endif
+
 /***************** BASE64 *****************/
 
 static const char code[]=
@@ -361,35 +365,26 @@
 
 /***************** end *****************/
 
-static const luaL_Reg Reg[] =
-{
-	{ NULL,		NULL	}
-};
-
 LUALIB_API int luaopen_util_encodings(lua_State *L)
 {
 #ifdef USE_STRINGPREP_ICU
 	init_icu();
 #endif
-	luaL_register(L, "encodings", Reg);
+	lua_newtable(L);
 
-	lua_pushliteral(L, "base64");
 	lua_newtable(L);
 	luaL_register(L, NULL, Reg_base64);
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "base64");
 
-	lua_pushliteral(L, "stringprep");
 	lua_newtable(L);
 	luaL_register(L, NULL, Reg_stringprep);
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "stringprep");
 
-	lua_pushliteral(L, "idna");
 	lua_newtable(L);
 	luaL_register(L, NULL, Reg_idna);
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "idna");
 
-	lua_pushliteral(L, "version");			/** version */
 	lua_pushliteral(L, "-3.14");
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "version");
 	return 1;
 }
--- a/util-src/hashes.c	Tue Feb 24 15:19:20 2015 +0000
+++ b/util-src/hashes.c	Tue Feb 24 15:22:09 2015 +0000
@@ -27,6 +27,10 @@
 #include <openssl/sha.h>
 #include <openssl/md5.h>
 
+#if (LUA_VERSION_NUM == 502)
+#define luaL_register(L, N, R) luaL_setfuncs(L, R, 0)
+#endif
+
 #define HMAC_IPAD 0x36363636
 #define HMAC_OPAD 0x5c5c5c5c
 
@@ -203,9 +207,9 @@
 
 LUALIB_API int luaopen_util_hashes(lua_State *L)
 {
-	luaL_register(L, "hashes", Reg);
-	lua_pushliteral(L, "version");			/** version */
+	lua_newtable(L);
+	luaL_register(L, NULL, Reg);
 	lua_pushliteral(L, "-3.14");
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "version");
 	return 1;
 }
--- a/util-src/net.c	Tue Feb 24 15:19:20 2015 +0000
+++ b/util-src/net.c	Tue Feb 24 15:22:09 2015 +0000
@@ -26,6 +26,10 @@
 #include <lua.h>
 #include <lauxlib.h>
 
+#if (LUA_VERSION_NUM == 502)
+#define luaL_register(L, N, R) luaL_setfuncs(L, R, 0)
+#endif
+
 /* Enumerate all locally configured IP addresses */
 
 const char * const type_strings[] = {
@@ -112,6 +116,7 @@
 		{ NULL, NULL }
 	};
 
-	luaL_register(L, "net",  exports);
+	lua_newtable(L);
+	luaL_register(L, NULL,  exports);
 	return 1;
 }
--- a/util-src/pposix.c	Tue Feb 24 15:19:20 2015 +0000
+++ b/util-src/pposix.c	Tue Feb 24 15:22:09 2015 +0000
@@ -35,6 +35,10 @@
 #include "lualib.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 502)
+#define luaL_register(L, N, R) luaL_setfuncs(L, R, 0)
+#endif
+
 #include <fcntl.h>
 #if defined(__linux__) && defined(_GNU_SOURCE)
 #include <linux/falloc.h>
@@ -491,11 +495,24 @@
 	return -1;
 }
 
+unsigned long int arg_to_rlimit(lua_State* L, int idx, rlim_t current) {
+	switch(lua_type(L, idx)) {
+	case LUA_TSTRING:
+		if(strcmp(lua_tostring(L, idx), "unlimited") == 0)
+			return RLIM_INFINITY;
+	case LUA_TNUMBER:
+		return lua_tointeger(L, idx);
+	case LUA_TNONE:
+	case LUA_TNIL:
+		return current;
+	default:
+		return luaL_argerror(L, idx, "unexpected type");
+	}
+}
+
 int lc_setrlimit(lua_State *L) {
+	struct rlimit lim;
 	int arguments = lua_gettop(L);
-	int softlimit = -1;
-	int hardlimit = -1;
-	const char *resource = NULL;
 	int rid = -1;
 	if(arguments < 1 || arguments > 3) {
 		lua_pushboolean(L, 0);
@@ -503,39 +520,28 @@
 		return 2;
 	}
 
-	resource = luaL_checkstring(L, 1);
-	softlimit = luaL_checkinteger(L, 2);
-	hardlimit = luaL_checkinteger(L, 3);
-
-	rid = string2resource(resource);
-	if (rid != -1) {
-		struct rlimit lim;
-		struct rlimit lim_current;
-
-		if (softlimit < 0 || hardlimit < 0) {
-			if (getrlimit(rid, &lim_current)) {
-				lua_pushboolean(L, 0);
-				lua_pushstring(L, "getrlimit-failed");
-				return 2;
-			}
-		}
-
-		if (softlimit < 0) lim.rlim_cur = lim_current.rlim_cur;
-			else lim.rlim_cur = softlimit;
-		if (hardlimit < 0) lim.rlim_max = lim_current.rlim_max;
-			else lim.rlim_max = hardlimit;
-
-		if (setrlimit(rid, &lim)) {
-			lua_pushboolean(L, 0);
-			lua_pushstring(L, "setrlimit-failed");
-			return 2;
-		}
-	} else {
-		/* Unsupported resoucrce. Sorry I'm pretty limited by POSIX standard. */
+	rid = string2resource(luaL_checkstring(L, 1));
+	if (rid == -1) {
 		lua_pushboolean(L, 0);
 		lua_pushstring(L, "invalid-resource");
 		return 2;
 	}
+
+	/* Fetch current values to use as defaults */
+	if (getrlimit(rid, &lim)) {
+		lua_pushboolean(L, 0);
+		lua_pushstring(L, "getrlimit-failed");
+		return 2;
+	}
+
+	lim.rlim_cur = arg_to_rlimit(L, 2, lim.rlim_cur);
+	lim.rlim_max = arg_to_rlimit(L, 3, lim.rlim_max);
+
+	if (setrlimit(rid, &lim)) {
+		lua_pushboolean(L, 0);
+		lua_pushstring(L, "setrlimit-failed");
+		return 2;
+	}
 	lua_pushboolean(L, 1);
 	return 1;
 }
@@ -552,6 +558,8 @@
 		return 2;
 	}
 
+
+
 	resource = luaL_checkstring(L, 1);
 	rid = string2resource(resource);
 	if (rid != -1) {
@@ -567,8 +575,14 @@
 		return 2;
 	}
 	lua_pushboolean(L, 1);
-	lua_pushnumber(L, lim.rlim_cur);
-	lua_pushnumber(L, lim.rlim_max);
+	if(lim.rlim_cur == RLIM_INFINITY)
+		lua_pushstring(L, "unlimited");
+	else
+		lua_pushnumber(L, lim.rlim_cur);
+	if(lim.rlim_max == RLIM_INFINITY)
+		lua_pushstring(L, "unlimited");
+	else
+		lua_pushnumber(L, lim.rlim_max);
 	return 3;
 }
 
@@ -758,7 +772,8 @@
 		{ NULL, NULL }
 	};
 
-	luaL_register(L, "pposix",  exports);
+	lua_newtable(L);
+	luaL_register(L, NULL,  exports);
 
 	lua_pushliteral(L, "pposix");
 	lua_setfield(L, -2, "_NAME");
--- a/util-src/signal.c	Tue Feb 24 15:19:20 2015 +0000
+++ b/util-src/signal.c	Tue Feb 24 15:22:09 2015 +0000
@@ -32,6 +32,10 @@
 #include "lua.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 502)
+#define luaL_register(L, N, R) luaL_setfuncs(L, R, 0)
+#endif
+
 #ifndef lsig
 
 #define lsig
@@ -384,13 +388,14 @@
   int i = 0;
 
   /* add the library */
-  luaL_register(L, "signal", lsignal_lib);
+  lua_newtable(L);
+  luaL_register(L, NULL, lsignal_lib);
 
   /* push lua_signals table into the registry */
   /* put the signals inside the library table too,
    * they are only a reference */
   lua_pushstring(L, LUA_SIGNAL);
-  lua_createtable(L, 0, 0);
+  lua_newtable(L);
 
   while (lua_signals[i].name != NULL)
   {
--- a/util-src/windows.c	Tue Feb 24 15:19:20 2015 +0000
+++ b/util-src/windows.c	Tue Feb 24 15:22:09 2015 +0000
@@ -19,6 +19,10 @@
 #include "lua.h"
 #include "lauxlib.h"
 
+#if (LUA_VERSION_NUM == 502)
+#define luaL_register(L, N, R) luaL_setfuncs(L, R, 0)
+#endif
+
 static int Lget_nameservers(lua_State *L) {
 	char stack_buffer[1024]; // stack allocated buffer
 	IP4_ARRAY* ips = (IP4_ARRAY*) stack_buffer;
@@ -81,9 +85,9 @@
 };
 
 LUALIB_API int luaopen_util_windows(lua_State *L) {
-	luaL_register(L, "windows", Reg);
-	lua_pushliteral(L, "version");			/** version */
+	lua_newtable(L);
+	luaL_register(L, NULL, Reg);
 	lua_pushliteral(L, "-3.14");
-	lua_settable(L,-3);
+	lua_setfield(L, -2, "version");
 	return 1;
 }
--- a/util/array.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/array.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -11,8 +11,10 @@
 
 local setmetatable = setmetatable;
 local math_random = math.random;
+local math_floor = math.floor;
 local pairs, ipairs = pairs, ipairs;
 local tostring = tostring;
+local type = type;
 
 local array = {};
 local array_base = {};
@@ -59,13 +61,13 @@
 			write = write + 1;
 		end
 	end
-	
+
 	if inplace and write <= start_length then
 		for i=write,start_length do
 			outa[i] = nil;
 		end
 	end
-	
+
 	return outa;
 end
 
@@ -84,6 +86,25 @@
 	return outa;
 end
 
+function array_base.reverse(outa, ina)
+	local len = #ina;
+	if ina == outa then
+		local middle = math_floor(len/2);
+		len = len + 1;
+		local o; -- opposite
+		for i = 1, middle do
+			o = len - i;
+			outa[i], outa[o] = outa[o], outa[i];
+		end
+	else
+		local off = len + 1;
+		for i = 1, len do
+			outa[i] = ina[off - i];
+		end
+	end
+	return outa;
+end
+
 --- These methods only mutate the array
 function array_methods:shuffle(outa, ina)
 	local len = #self;
@@ -94,15 +115,6 @@
 	return self;
 end
 
-function array_methods:reverse()
-	local len = #self-1;
-	for i=len,1,-1 do
-		self:push(self[i]);
-		self:pop(i);
-	end
-	return self;
-end
-
 function array_methods:append(array)
 	local len,len2  = #self, #array;
 	for i=1,len2 do
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/async.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,158 @@
+local log = require "util.logger".init("util.async");
+
+local function runner_continue(thread)
+	-- ASSUMPTION: runner is in 'waiting' state (but we don't have the runner to know for sure)
+	if coroutine.status(thread) ~= "suspended" then -- This should suffice
+		return false;
+	end
+	local ok, state, runner = coroutine.resume(thread);
+	if not ok then
+		local level = 0;
+		while debug.getinfo(thread, level, "") do level = level + 1; end
+		ok, runner = debug.getlocal(thread, level-1, 1);
+		local error_handler = runner.watchers.error;
+		if error_handler then error_handler(runner, debug.traceback(thread, state)); end
+	elseif state == "ready" then
+		-- If state is 'ready', it is our responsibility to update runner.state from 'waiting'.
+		-- We also have to :run(), because the queue might have further items that will not be
+		-- processed otherwise. FIXME: It's probably best to do this in a nexttick (0 timer).
+		runner.state = "ready";
+		runner:run();
+	end
+	return true;
+end
+
+local function waiter(num)
+	local thread = coroutine.running();
+	if not thread then
+		error("Not running in an async context, see http://prosody.im/doc/developers/async");
+	end
+	num = num or 1;
+	local waiting;
+	return function ()
+		if num == 0 then return; end -- already done
+		waiting = true;
+		coroutine.yield("wait");
+	end, function ()
+		num = num - 1;
+		if num == 0 and waiting then
+			runner_continue(thread);
+		elseif num < 0 then
+			error("done() called too many times");
+		end
+	end;
+end
+
+local function guarder()
+	local guards = {};
+	return function (id, func)
+		local thread = coroutine.running();
+		if not thread then
+			error("Not running in an async context, see http://prosody.im/doc/developers/async");
+		end
+		local guard = guards[id];
+		if not guard then
+			guard = {};
+			guards[id] = guard;
+			log("debug", "New guard!");
+		else
+			table.insert(guard, thread);
+			log("debug", "Guarded. %d threads waiting.", #guard)
+			coroutine.yield("wait");
+		end
+		local function exit()
+			local next_waiting = table.remove(guard, 1);
+			if next_waiting then
+				log("debug", "guard: Executing next waiting thread (%d left)", #guard)
+				runner_continue(next_waiting);
+			else
+				log("debug", "Guard off duty.")
+				guards[id] = nil;
+			end
+		end
+		if func then
+			func();
+			exit();
+			return;
+		end
+		return exit;
+	end;
+end
+
+local runner_mt = {};
+runner_mt.__index = runner_mt;
+
+local function runner_create_thread(func, self)
+	local thread = coroutine.create(function (self)
+		while true do
+			func(coroutine.yield("ready", self));
+		end
+	end);
+	assert(coroutine.resume(thread, self)); -- Start it up, it will return instantly to wait for the first input
+	return thread;
+end
+
+local empty_watchers = {};
+local function runner(func, watchers, data)
+	return setmetatable({ func = func, thread = false, state = "ready", notified_state = "ready",
+		queue = {}, watchers = watchers or empty_watchers, data = data }
+	, runner_mt);
+end
+
+function runner_mt:run(input)
+	if input ~= nil then
+		table.insert(self.queue, input);
+	end
+	if self.state ~= "ready" then
+		return true, self.state, #self.queue;
+	end
+
+	local q, thread = self.queue, self.thread;
+	if not thread or coroutine.status(thread) == "dead" then
+		thread = runner_create_thread(self.func, self);
+		self.thread = thread;
+	end
+
+	local n, state, err = #q, self.state, nil;
+	self.state = "running";
+	while n > 0 and state == "ready" do
+		local consumed;
+		for i = 1,n do
+			local input = q[i];
+			local ok, new_state = coroutine.resume(thread, input);
+			if not ok then
+				consumed, state, err = i, "ready", debug.traceback(thread, new_state);
+				self.thread = nil;
+				break;
+			elseif new_state == "wait" then
+				consumed, state = i, "waiting";
+				break;
+			end
+		end
+		if not consumed then consumed = n; end
+		if q[n+1] ~= nil then
+			n = #q;
+		end
+		for i = 1, n do
+			q[i] = q[consumed+i];
+		end
+		n = #q;
+	end
+	self.state = state;
+	if err or state ~= self.notified_state then
+		if err then
+			state = "error"
+		else
+			self.notified_state = state;
+		end
+		local handler = self.watchers[state];
+		if handler then handler(self, err); end
+	end
+	return true, state, n;
+end
+
+function runner_mt:enqueue(input)
+	table.insert(self.queue, input);
+end
+
+return { waiter = waiter, guarder = guarder, runner = runner };
--- a/util/caps.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/caps.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/dataforms.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/dataforms.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -38,7 +38,7 @@
 		form:tag("field", { type = field_type, var = field.name, label = field.label });
 
 		local value = (data and data[field.name]) or field.value;
-		
+
 		if value then
 			-- Add value, depending on type
 			if field_type == "hidden" then
@@ -102,11 +102,11 @@
 			end
 			form:up();
 		end
-		
+
 		if field.required then
 			form:tag("required"):up();
 		end
-		
+
 		-- Jump back up to list of fields
 		form:up();
 	end
--- a/util/datetime.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/datetime.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/debug.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/debug.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -24,11 +24,15 @@
 end
 module("debugx", package.seeall);
 
-function get_locals_table(level)
-	level = level + 1; -- Skip this function itself
+function get_locals_table(thread, level)
 	local locals = {};
 	for local_num = 1, math.huge do
-		local name, value = debug.getlocal(level, local_num);
+		local name, value;
+		if thread then
+			name, value = debug.getlocal(thread, level, local_num);
+		else
+			name, value = debug.getlocal(level+1, local_num);
+		end
 		if not name then break; end
 		table.insert(locals, { name = name, value = value });
 	end
@@ -88,19 +92,19 @@
 	for level = start_level, math.huge do
 		local info;
 		if thread then
-			info = debug.getinfo(thread, level+1);
+			info = debug.getinfo(thread, level);
 		else
 			info = debug.getinfo(level+1);
 		end
 		if not info then break; end
-		
+
 		levels[(level-start_level)+1] = {
 			level = level;
 			info = info;
-			locals = get_locals_table(level+1);
+			locals = get_locals_table(thread, level+(thread and 0 or 1));
 			upvalues = get_upvalues_table(info.func);
 		};
-	end	
+	end
 	return levels;
 end
 
@@ -134,15 +138,15 @@
 		return nil; -- debug.traceback() does this
 	end
 
-	level = level or 1;
+	level = level or 0;
 
 	message = message and (message.."\n") or "";
-	
-	-- +3 counts for this function, and the pcall() and wrapper above us
-	local levels = get_traceback_table(thread, level+3);
-	
+
+	-- +3 counts for this function, and the pcall() and wrapper above us, the +1... I don't know.
+	local levels = get_traceback_table(thread, level+(thread == nil and 4 or 0));
+
 	local last_source_desc;
-	
+
 	local lines = {};
 	for nlevel, level in ipairs(levels) do
 		local info = level.info;
@@ -171,9 +175,11 @@
 		nlevel = nlevel-1;
 		table.insert(lines, "\t"..(nlevel==0 and ">" or " ")..getstring(styles.level_num, "("..nlevel..") ")..line);
 		local npadding = (" "):rep(#tostring(nlevel));
-		local locals_str = string_from_var_table(level.locals, optimal_line_length, "\t            "..npadding);
-		if locals_str then
-			table.insert(lines, "\t    "..npadding.."Locals: "..locals_str);
+		if level.locals then
+			local locals_str = string_from_var_table(level.locals, optimal_line_length, "\t            "..npadding);
+			if locals_str then
+				table.insert(lines, "\t    "..npadding.."Locals: "..locals_str);
+			end
 		end
 		local upvalues_str = string_from_var_table(level.upvalues, optimal_line_length, "\t            "..npadding);
 		if upvalues_str then
--- a/util/dependencies.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/dependencies.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -35,7 +35,7 @@
 	print("");
 end
 
--- COMPAT w/pre-0.8 Debian: The Debian config file used to use 
+-- COMPAT w/pre-0.8 Debian: The Debian config file used to use
 -- util.ztact, which has been removed from Prosody in 0.8. This
 -- is to log an error for people who still use it, so they can
 -- update their configs.
@@ -58,9 +58,9 @@
 	end
 
 	local fatal;
-	
+
 	local lxp = softreq "lxp"
-	
+
 	if not lxp then
 		missingdep("luaexpat", {
 				["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-expat0";
@@ -69,9 +69,9 @@
 			});
 		fatal = true;
 	end
-	
+
 	local socket = softreq "socket"
-	
+
 	if not socket then
 		missingdep("luasocket", {
 				["Debian/Ubuntu"] = "sudo apt-get install liblua5.1-socket2";
@@ -80,7 +80,7 @@
 			});
 		fatal = true;
 	end
-	
+
 	local lfs, err = softreq "lfs"
 	if not lfs then
 		missingdep("luafilesystem", {
@@ -90,9 +90,9 @@
 		 	});
 		fatal = true;
 	end
-	
+
 	local ssl = softreq "ssl"
-	
+
 	if not ssl then
 		missingdep("LuaSec", {
 				["Debian/Ubuntu"] = "http://prosody.im/download/start#debian_and_ubuntu";
@@ -100,7 +100,7 @@
 				["Source"] = "http://www.inf.puc-rio.br/~brunoos/luasec/";
 			}, "SSL/TLS support will not be available");
 	end
-	
+
 	local encodings, err = softreq "util.encodings"
 	if not encodings then
 		if err:match("not found") then
--- a/util/events.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/events.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -60,11 +60,11 @@
 			remove_handler(event, handler);
 		end
 	end;
-	local function fire_event(event, ...)
-		local h = handlers[event];
+	local function fire_event(event_name, event_data)
+		local h = handlers[event_name];
 		if h then
 			for i=1,#h do
-				local ret = h[i](...);
+				local ret = h[i](event_data);
 				if ret ~= nil then return ret; end
 			end
 		end
--- a/util/filters.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/filters.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -16,7 +16,7 @@
 	if not session.filters then
 		local filters = {};
 		session.filters = filters;
-		
+
 		function session.filter(type, data)
 			local filter_list = filters[type];
 			if filter_list then
@@ -28,11 +28,11 @@
 			return data;
 		end
 	end
-	
+
 	for i=1,#new_filter_hooks do
 		new_filter_hooks[i](session);
 	end
-	
+
 	return session.filter;
 end
 
@@ -40,7 +40,7 @@
 	if not session.filters then
 		initialize(session);
 	end
-	
+
 	local filter_list = session.filters[type];
 	if not filter_list then
 		filter_list = {};
@@ -48,14 +48,14 @@
 	elseif filter_list[callback] then
 		return; -- Filter already added
 	end
-	
+
 	priority = priority or 0;
-	
+
 	local i = 0;
 	repeat
 		i = i + 1;
 	until not filter_list[i] or filter_list[filter_list[i]] < priority;
-	
+
 	t_insert(filter_list, i, callback);
 	filter_list[callback] = priority;
 end
--- a/util/helpers.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/helpers.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/hex.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,25 @@
+local s_char = string.char;
+local s_format = string.format;
+local s_gsub = string.gsub;
+
+local char_to_hex = {};
+local hex_to_char = {};
+
+do
+	local char, hex;
+	for i = 0,255 do
+		char, hex = s_char(i), s_format("%02x", i);
+		char_to_hex[char] = hex;
+		hex_to_char[hex] = char;
+	end
+end
+
+local function to(s)
+	return (s_gsub(s, ".", char_to_hex));
+end
+
+local function from(s)
+	return (s_gsub(s, "..", hex_to_char));
+end
+
+return { to = to, from = from }
--- a/util/hmac.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/hmac.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/import.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/import.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/ip.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/ip.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -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
@@ -82,7 +92,7 @@
 	if ip:match("^[0:]*1$") then
 		return 0x2;
 	-- Link-local unicast:
-	elseif ip:match("^[Ff][Ee][89ABab]") then 
+	elseif ip:match("^[Ff][Ee][89ABab]") then
 		return 0x2;
 	-- Site-local unicast:
 	elseif ip:match("^[Ff][Ee][CcDdEeFf]") 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	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/iterators.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -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,9 +23,9 @@
 		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
 	local i,max = 0, #results;
 	return function (results)
@@ -52,15 +56,15 @@
 -- Given an iterator, iterate only over unique items
 function it.unique(f, s, var)
 	local set = {};
-	
+
 	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;
@@ -69,14 +73,13 @@
 --[[ Return the number of items an iterator returns ]]--
 function it.count(f, s, var)
 	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
-	
+
 	return x;
 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
--- a/util/jid.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/jid.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -37,11 +37,7 @@
 split = _split;
 
 function bare(jid)
-	local node, host = _split(jid);
-	if node and host then
-		return node.."@"..host;
-	end
-	return host;
+	return jid and match(jid, "^[^/]+");
 end
 
 local function _prepped_split(jid)
@@ -65,30 +61,22 @@
 end
 prepped_split = _prepped_split;
 
-function prep(jid)
-	local node, host, resource = _prepped_split(jid);
-	if host then
-		if node then
-			host = node .. "@" .. host;
-		end
-		if resource then
-			host = host .. "/" .. resource;
-		end
+local function _join(node, host, resource)
+	if not host then return end
+	if node and resource then
+		return node.."@"..host.."/"..resource;
+	elseif node then
+		return node.."@"..host;
+	elseif resource then
+		return host.."/"..resource;
 	end
 	return host;
 end
+join = _join;
 
-function join(node, host, resource)
-	if node and host and resource then
-		return node.."@"..host.."/"..resource;
-	elseif node and host then
-		return node.."@"..host;
-	elseif host and resource then
-		return host.."/"..resource;
-	elseif host then
-		return host;
-	end
-	return nil; -- Invalid JID
+function prep(jid)
+	local node, host, resource = _prepped_split(jid);
+	return _join(node, host, resource);
 end
 
 function compare(jid, acl)
--- a/util/json.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/json.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -348,9 +348,9 @@
 function json.decode(json)
 	json = json:gsub("\\.", first_escape) -- get rid of all escapes except \uXXXX, making string parsing much simpler
 		--:gsub("[\r\n]", "\t"); -- \r\n\t are equivalent, we care about none of them, and none of them can be in strings
-	
+
 	-- TODO do encoding verification
-	
+
 	local val, index = _readvalue(json, 1);
 	if val == nil then return val, index; end
 	if json:find("[^ \t\r\n]", index) then return nil, "garbage at eof"; end
--- a/util/logger.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/logger.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/multitable.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/multitable.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/paths.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,44 @@
+local t_concat = table.concat;
+
+local path_sep = package.config:sub(1,1);
+
+local path_util = {}
+
+-- Helper function to resolve relative paths (needed by config)
+function path_util.resolve_relative_path(parent_path, path)
+	if path then
+		-- Some normalization
+		parent_path = parent_path:gsub("%"..path_sep.."+$", "");
+		path = path:gsub("^%.%"..path_sep.."+", "");
+
+		local is_relative;
+		if path_sep == "/" and path:sub(1,1) ~= "/" then
+			is_relative = true;
+		elseif path_sep == "\\" and (path:sub(1,1) ~= "/" and (path:sub(2,3) ~= ":\\" and path:sub(2,3) ~= ":/")) then
+			is_relative = true;
+		end
+		if is_relative then
+			return parent_path..path_sep..path;
+		end
+	end
+	return path;
+end
+
+-- Helper function to convert a glob to a Lua pattern
+function path_util.glob_to_pattern(glob)
+	return "^"..glob:gsub("[%p*?]", function (c)
+		if c == "*" then
+			return ".*";
+		elseif c == "?" then
+			return ".";
+		else
+			return "%"..c;
+		end
+	end).."$";
+end
+
+function path_util.join(...)
+	return t_concat({...}, path_sep);
+end
+
+return path_util;
--- a/util/pluginloader.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/pluginloader.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/prosodyctl.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/prosodyctl.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -193,8 +193,8 @@
 		return false, "invalid-pidfile";
 	end
 	
-	local modules_enabled = set.new(config.get("*", "modules_enabled"));
-	if not modules_enabled:contains("posix") then
+	local modules_enabled = set.new(config.get("*", "modules_disabled"));
+	if prosody.platform ~= "posix" or modules_enabled:contains("posix") then
 		return false, "no-posix";
 	end
 	
--- a/util/pubsub.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/pubsub.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,23 +1,29 @@
 local events = require "util.events";
+local t_remove = table.remove;
 
 module("pubsub", package.seeall);
 
 local service = {};
 local service_mt = { __index = service };
 
-local default_config = {
+local default_config = { __index = {
 	broadcaster = function () end;
 	get_affiliation = function () end;
 	capabilities = {};
-};
+} };
+local default_node_config = { __index = {
+	["pubsub#max_items"] = "20";
+} };
 
 function new(config)
 	config = config or {};
 	return setmetatable({
-		config = setmetatable(config, { __index = default_config });
+		config = setmetatable(config, default_config);
+		node_defaults = setmetatable(config.node_defaults or {}, default_node_config);
 		affiliations = {};
 		subscriptions = {};
 		nodes = {};
+		data = {};
 		events = events.new();
 	}, service_mt);
 end
@@ -29,13 +35,13 @@
 
 function service:may(node, actor, action)
 	if actor == true then return true; end
-	
+
 	local node_obj = self.nodes[node];
 	local node_aff = node_obj and node_obj.affiliations[actor];
 	local service_aff = self.affiliations[actor]
 	                 or self.config.get_affiliation(actor, node, action)
 	                 or "none";
-	
+
 	-- Check if node allows/forbids it
 	local node_capabilities = node_obj and node_obj.capabilities;
 	if node_capabilities then
@@ -47,7 +53,7 @@
 			end
 		end
 	end
-	
+
 	-- Check service-wide capabilities instead
 	local service_capabilities = self.config.capabilities;
 	local caps = service_capabilities[node_aff or service_aff];
@@ -57,7 +63,7 @@
 			return can;
 		end
 	end
-	
+
 	return false;
 end
 
@@ -202,7 +208,7 @@
 	return true, node_obj.subscribers[jid];
 end
 
-function service:create(node, actor)
+function service:create(node, actor, options)
 	-- Access checking
 	if not self:may(node, actor, "create") then
 		return false, "forbidden";
@@ -211,17 +217,20 @@
 	if self.nodes[node] then
 		return false, "conflict";
 	end
-	
+
+	self.data[node] = {};
 	self.nodes[node] = {
 		name = node;
 		subscribers = {};
-		config = {};
-		data = {};
+		config = setmetatable(options or {}, {__index=self.node_defaults});
 		affiliations = {};
 	};
+	setmetatable(self.nodes[node], { __index = { data = self.data[node] } }); -- COMPAT
+	self.events.fire_event("node-created", { node = node, actor = actor });
 	local ok, err = self:set_affiliation(node, true, actor, "owner");
 	if not ok then
 		self.nodes[node] = nil;
+		self.data[node] = nil;
 	end
 	return ok, err;
 end
@@ -237,10 +246,31 @@
 		return false, "item-not-found";
 	end
 	self.nodes[node] = nil;
+	self.data[node] = nil;
+	self.events.fire_event("node-deleted", { node = node, actor = actor });
 	self.config.broadcaster("delete", node, node_obj.subscribers);
 	return true;
 end
 
+local function remove_item_by_id(data, id)
+	if not data[id] then return end
+	data[id] = nil;
+	for i, _id in ipairs(data) do
+		if id == _id then
+			t_remove(data, i);
+			return i;
+		end
+	end
+end
+
+local function trim_items(data, max)
+	max = tonumber(max);
+	if not max or #data <= max then return end
+	repeat
+		data[t_remove(data, 1)] = nil;
+	until #data <= max
+end
+
 function service:publish(node, actor, id, item)
 	-- Access checking
 	if not self:may(node, actor, "publish") then
@@ -258,9 +288,13 @@
 		end
 		node_obj = self.nodes[node];
 	end
-	node_obj.data[id] = item;
+	local node_data = self.data[node];
+	remove_item_by_id(node_data, id);
+	node_data[#node_data + 1] = id;
+	node_data[id] = item;
+	trim_items(node_data, node_obj.config["pubsub#max_items"]);
 	self.events.fire_event("item-published", { node = node, actor = actor, id = id, item = item });
-	self.config.broadcaster("items", node, node_obj.subscribers, item);
+	self.config.broadcaster("items", node, node_obj.subscribers, item, actor);
 	return true;
 end
 
@@ -271,10 +305,11 @@
 	end
 	--
 	local node_obj = self.nodes[node];
-	if (not node_obj) or (not node_obj.data[id]) then
+	if (not node_obj) or (not self.data[node][id]) then
 		return false, "item-not-found";
 	end
-	node_obj.data[id] = nil;
+	self.events.fire_event("item-retracted", { node = node, actor = actor, id = id });
+	remove_item_by_id(self.data[node], id);
 	if retract then
 		self.config.broadcaster("items", node, node_obj.subscribers, retract);
 	end
@@ -291,7 +326,8 @@
 	if not node_obj then
 		return false, "item-not-found";
 	end
-	node_obj.data = {}; -- Purge
+	self.data[node] = {}; -- Purge
+	self.events.fire_event("node-purged", { node = node, actor = actor });
 	if notify then
 		self.config.broadcaster("purge", node, node_obj.subscribers);
 	end
@@ -309,9 +345,9 @@
 		return false, "item-not-found";
 	end
 	if id then -- Restrict results to a single specific item
-		return true, { [id] = node_obj.data[id] };
+		return true, { id, [id] = self.data[node][id] };
 	else
-		return true, node_obj.data;
+		return true, self.data[node];
 	end
 end
 
@@ -388,4 +424,22 @@
 	return true;
 end
 
+function service:set_node_config(node, actor, new_config)
+	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
+
+	for k,v in pairs(new_config) do
+		node_obj.config[k] = v;
+	end
+	trim_items(self.data[node], node_obj.config["pubsub#max_items"]);
+
+	return true;
+end
+
 return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/random.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,43 @@
+-- Prosody IM
+-- Copyright (C) 2008-2014 Matthew Wild
+-- Copyright (C) 2008-2014 Waqas Hussain
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local tostring = tostring;
+local os_time = os.time;
+local os_clock = os.clock;
+local ceil = math.ceil;
+local H = require "util.hashes".sha512;
+
+local last_uniq_time = 0;
+local function uniq_time()
+	local new_uniq_time = os_time();
+	if last_uniq_time >= new_uniq_time then new_uniq_time = last_uniq_time + 1; end
+	last_uniq_time = new_uniq_time;
+	return new_uniq_time;
+end
+
+local function new_random(x)
+	return H(x..os_clock()..tostring({}));
+end
+
+local buffer = new_random(uniq_time());
+
+local function seed(x)
+	buffer = new_random(buffer..x);
+end
+
+local function bytes(n)
+	if #buffer < n+4 then seed(uniq_time()); end
+	local r = buffer:sub(1, n);
+	buffer = buffer:sub(n+1);
+	return r;
+end
+
+return {
+	seed = seed;
+	bytes = bytes;
+};
--- a/util/sasl.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/sasl.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -27,19 +27,38 @@
 state = false : disabled
 state = true : enabled
 state = nil : non-existant
+
+Channel Binding:
+
+To enable support of channel binding in some mechanisms you need to provide appropriate callbacks in a table
+at profile.cb.
+
+Example:
+	profile.cb["tls-unique"] = function(self)
+		return self.user
+	end
+
 ]]
 
 local method = {};
 method.__index = method;
 local mechanisms = {};
 local backend_mechanism = {};
+local mechanism_channelbindings = {};
 
 -- register a new SASL mechanims
-function registerMechanism(name, backends, f)
+function registerMechanism(name, backends, f, cb_backends)
 	assert(type(name) == "string", "Parameter name MUST be a string.");
 	assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table.");
 	assert(type(f) == "function", "Parameter f MUST be a function.");
+	if cb_backends then assert(type(cb_backends) == "table"); end
 	mechanisms[name] = f
+	if cb_backends then
+		mechanism_channelbindings[name] = {};
+		for _, cb_name in ipairs(cb_backends) do
+			mechanism_channelbindings[name][cb_name] = true;
+		end
+	end
 	for _, backend_name in ipairs(backends) do
 		if backend_mechanism[backend_name] == nil then backend_mechanism[backend_name] = {}; end
 		t_insert(backend_mechanism[backend_name], name);
@@ -63,6 +82,15 @@
 	return setmetatable({ profile = profile, realm = realm, mechs = mechanisms }, method);
 end
 
+-- add a channel binding handler
+function method:add_cb_handler(name, f)
+	if type(self.profile.cb) ~= "table" then
+		self.profile.cb = {};
+	end
+	self.profile.cb[name] = f;
+	return self;
+end
+
 -- get a fresh clone with the same realm and profile
 function method:clean_clone()
 	return new(self.realm, self.profile)
@@ -70,7 +98,23 @@
 
 -- get a list of possible SASL mechanims to use
 function method:mechanisms()
-	return self.mechs;
+	local current_mechs = {};
+	for mech, _ in pairs(self.mechs) do
+		if mechanism_channelbindings[mech] then
+			if self.profile.cb then
+				local ok = false;
+				for cb_name, _ in pairs(self.profile.cb) do
+					if mechanism_channelbindings[mech][cb_name] then
+						ok = true;
+					end
+				end
+				if ok == true then current_mechs[mech] = true; end
+			end
+		else
+			current_mechs[mech] = true;
+		end
+	end
+	return current_mechs;
 end
 
 -- select a mechanism to use
@@ -92,5 +136,6 @@
 require "util.sasl.digest-md5".init(registerMechanism);
 require "util.sasl.anonymous" .init(registerMechanism);
 require "util.sasl.scram"     .init(registerMechanism);
+require "util.sasl.external"  .init(registerMechanism);
 
 return _M;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/sasl/external.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,25 @@
+local saslprep = require "util.encodings".stringprep.saslprep;
+
+module "sasl.external"
+
+local function external(self, message)
+	message = saslprep(message);
+	local state
+	self.username, state = self.profile.external(message);
+
+	if state == false then
+		return "failure", "account-disabled";
+	elseif state == nil  then
+		return "failure", "not-authorized";
+	elseif state == "expired" then
+		return "false", "credentials-expired";
+	end
+
+	return "success";
+end
+
+function init(registerMechanism)
+	registerMechanism("EXTERNAL", {"external"}, external);
+end
+
+return _M;
--- a/util/sasl/scram.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/sasl/scram.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -13,7 +13,6 @@
 
 local s_match = string.match;
 local type = type
-local string = string
 local base64 = require "util.encodings".base64;
 local hmac_sha1 = require "util.hashes".hmac_sha1;
 local sha1 = require "util.hashes".sha1;
@@ -39,18 +38,14 @@
 	function(username, realm)
 		return stored_key, server_key, iteration_count, salt, state;
 	end
+
+Supported Channel Binding Backends
+
+'tls-unique' according to RFC 5929
 ]]
 
 local default_i = 4096
 
-local function bp( b )
-	local result = ""
-	for i=1, b:len() do
-		result = result.."\\"..b:byte(i)
-	end
-	return result
-end
-
 local xor_map = {0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;1;0;3;2;5;4;7;6;9;8;11;10;13;12;15;14;2;3;0;1;6;7;4;5;10;11;8;9;14;15;12;13;3;2;1;0;7;6;5;4;11;10;9;8;15;14;13;12;4;5;6;7;0;1;2;3;12;13;14;15;8;9;10;11;5;4;7;6;1;0;3;2;13;12;15;14;9;8;11;10;6;7;4;5;2;3;0;1;14;15;12;13;10;11;8;9;7;6;5;4;3;2;1;0;15;14;13;12;11;10;9;8;8;9;10;11;12;13;14;15;0;1;2;3;4;5;6;7;9;8;11;10;13;12;15;14;1;0;3;2;5;4;7;6;10;11;8;9;14;15;12;13;2;3;0;1;6;7;4;5;11;10;9;8;15;14;13;12;3;2;1;0;7;6;5;4;12;13;14;15;8;9;10;11;4;5;6;7;0;1;2;3;13;12;15;14;9;8;11;10;5;4;7;6;1;0;3;2;14;15;12;13;10;11;8;9;6;7;4;5;2;3;0;1;15;14;13;12;11;10;9;8;7;6;5;4;3;2;1;0;};
 
 local result = {};
@@ -73,11 +68,11 @@
 			return false
 		end
 	end
-	
+
 	-- replace =2C with , and =3D with =
 	username = username:gsub("=2C", ",");
 	username = username:gsub("=3D", "=");
-	
+
 	-- apply SASLprep
 	username = saslprep(username);
 
@@ -106,96 +101,131 @@
 end
 
 local function scram_gen(hash_name, H_f, HMAC_f)
+	local profile_name = "scram_" .. hashprep(hash_name);
 	local function scram_hash(self, message)
-		if not self.state then self["state"] = {} end
-	
+		local support_channel_binding = false;
+		if self.profile.cb then support_channel_binding = true; end
+
 		if type(message) ~= "string" or #message == 0 then return "failure", "malformed-request" end
-		if not self.state.name then
+		local state = self.state;
+		if not state then
 			-- we are processing client_first_message
 			local client_first_message = message;
-			
+
 			-- TODO: fail if authzid is provided, since we don't support them yet
-			self.state["client_first_message"] = client_first_message;
-			self.state["gs2_cbind_flag"], self.state["authzid"], self.state["name"], self.state["clientnonce"]
-				= client_first_message:match("^(%a),(.*),n=(.*),r=([^,]*).*");
+			local gs2_header, gs2_cbind_flag, gs2_cbind_name, authzid, client_first_message_bare, username, clientnonce
+				= s_match(client_first_message, "^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$");
 
-			-- we don't do any channel binding yet
-			if self.state.gs2_cbind_flag ~= "n" and self.state.gs2_cbind_flag ~= "y" then
+			if not gs2_cbind_flag then
 				return "failure", "malformed-request";
 			end
 
-			if not self.state.name or not self.state.clientnonce then
-				return "failure", "malformed-request", "Channel binding isn't support at this time.";
+			if support_channel_binding and gs2_cbind_flag == "y" then
+				-- "y" -> client does support channel binding
+				--        but thinks the server does not.
+					return "failure", "malformed-request";
+				end
+
+			if gs2_cbind_flag == "n" then
+				-- "n" -> client doesn't support channel binding.
+				support_channel_binding = false;
 			end
-		
-			self.state.name = validate_username(self.state.name, self.profile.nodeprep);
-			if not self.state.name then
+
+			if support_channel_binding and gs2_cbind_flag == "p" then
+				-- check whether we support the proposed channel binding type
+				if not self.profile.cb[gs2_cbind_name] then
+					return "failure", "malformed-request", "Proposed channel binding type isn't supported.";
+				end
+			else
+				-- no channel binding,
+				gs2_cbind_name = nil;
+			end
+
+			username = validate_username(username, self.profile.nodeprep);
+			if not username then
 				log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
 				return "failure", "malformed-request", "Invalid username.";
 			end
-		
-			self.state["servernonce"] = generate_uuid();
-			
+
 			-- retreive credentials
+			local stored_key, server_key, salt, iteration_count;
 			if self.profile.plain then
-				local password, state = self.profile.plain(self, self.state.name, self.realm)
+				local password, state = self.profile.plain(self, username, self.realm)
 				if state == nil then return "failure", "not-authorized"
 				elseif state == false then return "failure", "account-disabled" end
-				
+
 				password = saslprep(password);
 				if not password then
 					log("debug", "Password violates SASLprep.");
 					return "failure", "not-authorized", "Invalid password."
 				end
 
-				self.state.salt = generate_uuid();
-				self.state.iteration_count = default_i;
+				salt = generate_uuid();
+				iteration_count = default_i;
 
 				local succ = false;
-				succ, self.state.stored_key, self.state.server_key = getAuthenticationDatabaseSHA1(password, self.state.salt, default_i, self.state.iteration_count);
+				succ, stored_key, server_key = getAuthenticationDatabaseSHA1(password, salt, iteration_count);
 				if not succ then
-					log("error", "Generating authentication database failed. Reason: %s", self.state.stored_key);
+					log("error", "Generating authentication database failed. Reason: %s", stored_key);
 					return "failure", "temporary-auth-failure";
 				end
-			elseif self.profile["scram_"..hashprep(hash_name)] then
-				local stored_key, server_key, iteration_count, salt, state = self.profile["scram_"..hashprep(hash_name)](self, self.state.name, self.realm);
+			elseif self.profile[profile_name] then
+				local state;
+				stored_key, server_key, iteration_count, salt, state = self.profile[profile_name](self, username, self.realm);
 				if state == nil then return "failure", "not-authorized"
 				elseif state == false then return "failure", "account-disabled" end
-				
-				self.state.stored_key = stored_key;
-				self.state.server_key = server_key;
-				self.state.iteration_count = iteration_count;
-				self.state.salt = salt
 			end
-		
-			local server_first_message = "r="..self.state.clientnonce..self.state.servernonce..",s="..base64.encode(self.state.salt)..",i="..self.state.iteration_count;
-			self.state["server_first_message"] = server_first_message;
+
+			local nonce = clientnonce .. generate_uuid();
+			local server_first_message = "r="..nonce..",s="..base64.encode(salt)..",i="..iteration_count;
+			self.state = {
+				gs2_header = gs2_header;
+				gs2_cbind_name = gs2_cbind_name;
+				username = username;
+				nonce = nonce;
+
+				server_key = server_key;
+				stored_key = stored_key;
+				client_first_message_bare = client_first_message_bare;
+				server_first_message = server_first_message;
+			}
 			return "challenge", server_first_message
 		else
 			-- we are processing client_final_message
 			local client_final_message = message;
-			
-			self.state["channelbinding"], self.state["nonce"], self.state["proof"] = client_final_message:match("^c=(.*),r=(.*),.*p=(.*)");
-	
-			if not self.state.proof or not self.state.nonce or not self.state.channelbinding then
+
+			local client_final_message_without_proof, channelbinding, nonce, proof
+				= s_match(client_final_message, "(c=([^,]*),r=([^,]*),?.-),p=(.*)$");
+
+			if not proof or not nonce or not channelbinding then
 				return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message.";
 			end
 
-			if self.state.nonce ~= self.state.clientnonce..self.state.servernonce then
+			local client_gs2_header = base64.decode(channelbinding)
+			local our_client_gs2_header = state["gs2_header"]
+			if state.gs2_cbind_name then
+				-- we support channelbinding, so check if the value is valid
+				our_client_gs2_header = our_client_gs2_header .. self.profile.cb[state.gs2_cbind_name](self);
+			end
+			if client_gs2_header ~= our_client_gs2_header then
+				return "failure", "malformed-request", "Invalid channel binding value.";
+			end
+
+			if nonce ~= state.nonce then
 				return "failure", "malformed-request", "Wrong nonce in client-final-message.";
 			end
-			
-			local ServerKey = self.state.server_key;
-			local StoredKey = self.state.stored_key;
-			
-			local AuthMessage = "n=" .. s_match(self.state.client_first_message,"n=(.+)") .. "," .. self.state.server_first_message .. "," .. s_match(client_final_message, "(.+),p=.+")
+
+			local ServerKey = state.server_key;
+			local StoredKey = state.stored_key;
+
+			local AuthMessage = state.client_first_message_bare .. "," .. state.server_first_message .. "," .. client_final_message_without_proof
 			local ClientSignature = HMAC_f(StoredKey, AuthMessage)
-			local ClientKey = binaryXOR(ClientSignature, base64.decode(self.state.proof))
+			local ClientKey = binaryXOR(ClientSignature, base64.decode(proof))
 			local ServerSignature = HMAC_f(ServerKey, AuthMessage)
 
 			if StoredKey == H_f(ClientKey) then
 				local server_final_message = "v="..base64.encode(ServerSignature);
-				self["username"] = self.state.name;
+				self["username"] = state.username;
 				return "success", server_final_message;
 			else
 				return "failure", "not-authorized", "The response provided by the client doesn't match the one we calculated.";
@@ -208,6 +238,9 @@
 function init(registerMechanism)
 	local function registerSCRAMMechanism(hash_name, hash, hmac_hash)
 		registerMechanism("SCRAM-"..hash_name, {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash));
+
+		-- register channel binding equivalent
+		registerMechanism("SCRAM-"..hash_name.."-PLUS", {"plain", "scram_"..(hashprep(hash_name))}, scram_gen(hash_name:lower(), hash, hmac_hash), {"tls-unique"});
 	end
 
 	registerSCRAMMechanism("SHA-1", sha1, hmac_sha1);
--- a/util/sasl_cyrus.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/sasl_cyrus.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -78,10 +78,10 @@
 end
 
 -- create a new SASL object which can be used to authenticate clients
--- host_fqdn may be nil in which case gethostname() gives the value. 
+-- host_fqdn may be nil in which case gethostname() gives the value.
 --      For GSSAPI, this determines the hostname in the service ticket (after
 --      reverse DNS canonicalization, only if [libdefaults] rdns = true which
---      is the default).  
+--      is the default).
 function new(realm, service_name, app_name, host_fqdn)
 
 	init(app_name or service_name);
--- a/util/serialization.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/serialization.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/set.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/set.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -40,13 +40,13 @@
 			return false;
 		end
 	end
-	
+
 	for item in pairs(set2) do
 		if not set1[item] then
 			return false;
 		end
 	end
-	
+
 	return true;
 end
 function set_mt.__tostring(set)
@@ -65,23 +65,23 @@
 function new(list)
 	local items = setmetatable({}, items_mt);
 	local set = { _items = items };
-	
+
 	function set:add(item)
 		items[item] = true;
 	end
-	
+
 	function set:contains(item)
 		return items[item];
 	end
-	
+
 	function set:items()
-		return items;
+		return next, items;
 	end
-	
+
 	function set:remove(item)
 		items[item] = nil;
 	end
-	
+
 	function set:add_list(list)
 		if list then
 			for _, item in ipairs(list) do
@@ -89,7 +89,7 @@
 			end
 		end
 	end
-	
+
 	function set:include(otherset)
 		for item in otherset do
 			items[item] = true;
@@ -101,22 +101,22 @@
 			items[item] = nil;
 		end
 	end
-	
+
 	function set:empty()
 		return not next(items);
 	end
-	
+
 	if list then
 		set:add_list(list);
 	end
-	
+
 	return setmetatable(set, set_mt);
 end
 
 function union(set1, set2)
 	local set = new();
 	local items = set._items;
-	
+
 	for item in pairs(set1._items) do
 		items[item] = true;
 	end
@@ -124,14 +124,14 @@
 	for item in pairs(set2._items) do
 		items[item] = true;
 	end
-	
+
 	return set;
 end
 
 function difference(set1, set2)
 	local set = new();
 	local items = set._items;
-	
+
 	for item in pairs(set1._items) do
 		items[item] = (not set2._items[item]) or nil;
 	end
@@ -142,13 +142,13 @@
 function intersection(set1, set2)
 	local set = new();
 	local items = set._items;
-	
+
 	set1, set2 = set1._items, set2._items;
-	
+
 	for item in pairs(set1) do
 		items[item] = (not not set2[item]) or nil;
 	end
-	
+
 	return set;
 end
 
--- a/util/sql.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/sql.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -45,7 +45,7 @@
 };
 
 local functions = {
-	
+
 };
 
 local cmap = {
@@ -156,6 +156,7 @@
 	dbh:autocommit(false); -- don't commit automatically
 	self.conn = dbh;
 	self.prepared = {};
+	self:set_encoding();
 	return true;
 end
 function engine:execute(sql, ...)
@@ -177,8 +178,8 @@
 end
 
 local result_mt = { __index = {
-	affected = function(self) return self.__affected; end;
-	rowcount = function(self) return self.__rowcount; end;
+	affected = function(self) return self.__stmt:affected(); end;
+	rowcount = function(self) return self.__stmt:rowcount(); end;
 } };
 
 function engine:execute_query(sql, ...)
@@ -200,7 +201,7 @@
 		prepared[sql] = stmt;
 	end
 	assert(stmt:execute(...));
-	return setmetatable({ __affected = stmt:affected(), __rowcount = stmt:rowcount() }, result_mt);
+	return setmetatable({ __stmt = stmt }, result_mt);
 end
 engine.insert = engine.execute_update;
 engine.select = engine.execute_query;
@@ -251,19 +252,39 @@
 	elseif self.params.driver == "MySQL" then
 		sql = sql:gsub("`([,)])", "`(20)%1");
 	end
+	if index.unique then
+		sql = sql:gsub("^CREATE", "CREATE UNIQUE");
+	end
 	--print(sql);
 	return self:execute(sql);
 end
 function engine:_create_table(table)
 	local sql = "CREATE TABLE `"..table.name.."` (";
 	for i,col in ipairs(table.c) do
-		sql = sql.."`"..col.name.."` "..col.type;
+		local col_type = col.type;
+		if col_type == "MEDIUMTEXT" and self.params.driver ~= "MySQL" then
+			col_type = "TEXT"; -- MEDIUMTEXT is MySQL-specific
+		end
+		if col.auto_increment == true and self.params.driver == "PostgreSQL" then
+			col_type = "BIGSERIAL";
+		end
+		sql = sql.."`"..col.name.."` "..col_type;
 		if col.nullable == false then sql = sql.." NOT NULL"; end
+		if col.primary_key == true then sql = sql.." PRIMARY KEY"; end
+		if col.auto_increment == true then
+			if self.params.driver == "MySQL" then
+				sql = sql.." AUTO_INCREMENT";
+			elseif self.params.driver == "SQLite3" then
+				sql = sql.." AUTOINCREMENT";
+			end
+		end
 		if i ~= #table.c then sql = sql..", "; end
 	end
 	sql = sql.. ");"
 	if self.params.driver == "PostgreSQL" then
 		sql = sql:gsub("`", "\"");
+	elseif self.params.driver == "MySQL" then
+		sql = sql:gsub(";$", " CHARACTER SET 'utf8' COLLATE 'utf8_bin';");
 	end
 	local success,err = self:execute(sql);
 	if not success then return success,err; end
@@ -274,6 +295,28 @@
 	end
 	return success;
 end
+function engine:set_encoding() -- to UTF-8
+	local driver = self.params.driver;
+	if driver == "SQLite3" then
+		return self:transaction(function()
+			if self:select"PRAGMA encoding;"()[1] == "UTF-8" then
+				self.charset = "utf8";
+			end
+		end);
+	end
+	local set_names_query = "SET NAMES '%s';"
+	local charset = "utf8";
+	if driver == "MySQL" then
+		set_names_query = set_names_query:gsub(";$", " COLLATE 'utf8_bin';");
+		local ok, charsets = self:transaction(function()
+			return self:select"SELECT `CHARACTER_SET_NAME` FROM `information_schema`.`CHARACTER_SETS` WHERE `CHARACTER_SET_NAME` LIKE 'utf8%' ORDER BY MAXLEN DESC LIMIT 1;";
+		end);
+		local row = ok and charsets();
+		charset = row and row[1] or charset;
+	end
+	self.charset = charset;
+	return self:transaction(function() return self:execute(set_names_query:format(charset)); end);
+end
 local engine_mt = { __index = engine };
 
 local function db2uri(params)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/sslconfig.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,87 @@
+
+local handlers = { };
+local finalisers = { };
+local id = function (v) return v end
+
+function handlers.options(a, k, b)
+	local o = a[k] or { };
+	if type(b) ~= "table" then b = { b } end
+	for k,v in pairs(b) do
+		if v == true or v == false then
+			o[k] = v;
+		else
+			o[v] = true;
+		end
+	end
+	a[k] = o;
+end
+
+handlers.verify = handlers.options;
+handlers.verifyext = handlers.options;
+
+function finalisers.options(a)
+	local o = {};
+	for opt, enable in pairs(a) do
+		if enable then
+			o[#o+1] = opt;
+		end
+	end
+	return o;
+end
+
+finalisers.verify = finalisers.options;
+finalisers.verifyext = finalisers.options;
+
+function finalisers.ciphers(a)
+	if type(a) == "table" then
+		return table.concat(a, ":");
+	end
+	return a;
+end
+
+local protocols = { "sslv2", "sslv3", "tlsv1", "tlsv1_1", "tlsv1_2" };
+for i = 1, #protocols do protocols[protocols[i] .. "+"] = i - 1; end
+
+local function protocol(a)
+	local min_protocol = protocols[a.protocol];
+	if min_protocol then
+		a.protocol = "sslv23";
+		for i = 1, min_protocol do
+			table.insert(a.options, "no_"..protocols[i]);
+		end
+	end
+end
+
+local function apply(a, b)
+	if type(b) == "table" then
+		for k,v in pairs(b) do
+			(handlers[k] or rawset)(a, k, v);
+		end
+	end
+end
+
+local function final(a)
+	local f = { };
+	for k,v in pairs(a) do
+		f[k] = (finalisers[k] or id)(v);
+	end
+	protocol(f);
+	return f;
+end
+
+local sslopts_mt = {
+	__index = {
+		apply = apply;
+		final = final;
+	};
+};
+
+local function new()
+	return setmetatable({options={}}, sslopts_mt);
+end
+
+return {
+	apply = apply;
+	final = final;
+	new = new;
+};
--- a/util/stanza.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/stanza.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -99,7 +99,7 @@
 		if (not name or child.name == name)
 			and ((not xmlns and self.attr.xmlns == child.attr.xmlns)
 				or child.attr.xmlns == xmlns) then
-			
+
 			return child;
 		end
 	end
@@ -152,7 +152,7 @@
 function stanza_mt:maptags(callback)
 	local tags, curr_tag = self.tags, 1;
 	local n_children, n_tags = #self, #tags;
-	
+
 	local i = 1;
 	while curr_tag <= n_tags and n_tags > 0 do
 		if self[i] == tags[curr_tag] then
@@ -258,13 +258,13 @@
 
 function stanza_mt.get_error(stanza)
 	local type, condition, text;
-	
+
 	local error_tag = stanza:get_child("error");
 	if not error_tag then
 		return nil, nil, nil;
 	end
 	type = error_tag.attr.type;
-	
+
 	for _, child in ipairs(error_tag.tags) do
 		if child.attr.xmlns == xmlns_stanzas then
 			if not text and child.name == "text" then
@@ -333,7 +333,7 @@
 			stanza.tags = tags;
 		end
 	end
-	
+
 	return stanza;
 end
 
@@ -390,7 +390,7 @@
 	local style_attrv = getstyle("red");
 	local style_tagname = getstyle("red");
 	local style_punc = getstyle("magenta");
-	
+
 	local attr_format = " "..getstring(style_attrk, "%s")..getstring(style_punc, "=")..getstring(style_attrv, "'%s'");
 	local top_tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">");
 	--local tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">").."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
@@ -411,7 +411,7 @@
 		end
 		return s_format(tag_format, t.name, attr_string, children_text, t.name);
 	end
-	
+
 	function stanza_mt.pretty_top_tag(t)
 		local attr_string = "";
 		if t.attr then
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/statistics.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -0,0 +1,160 @@
+local t_sort = table.sort
+local m_floor = math.floor;
+local time = require "socket".gettime;
+
+local function nop_function() end
+
+local function percentile(arr, length, pc)
+	local n = pc/100 * (length + 1);
+	local k, d = m_floor(n), n%1;
+	if k == 0 then
+		return arr[1] or 0;
+	elseif k >= length then
+		return arr[length];
+	end
+	return arr[k] + d*(arr[k+1] - arr[k]);
+end
+
+local function new_registry(config)
+	config = config or {};
+	local duration_sample_interval = config.duration_sample_interval or 5;
+	local duration_max_samples = config.duration_max_stored_samples or 5000;
+
+	local function get_distribution_stats(events, n_actual_events, since, new_time, units)
+		local n_stored_events = #events;
+		t_sort(events);
+		local sum = 0;
+		for i = 1, n_stored_events do
+			sum = sum + events[i];
+		end
+
+		return {
+			samples = events;
+			sample_count = n_stored_events;
+			count = n_actual_events,
+			rate = n_actual_events/(new_time-since);
+			average = n_stored_events > 0 and sum/n_stored_events or 0,
+			min = events[1] or 0,
+			max = events[n_stored_events] or 0,
+			units = units,
+		};
+	end
+
+
+	local registry = {};
+	local methods;
+	methods = {
+		amount = function (name, initial)
+			local v = initial or 0;
+			registry[name..":amount"] = function () return "amount", v; end
+			return function (new_v) v = new_v; end
+		end;
+		counter = function (name, initial)
+			local v = initial or 0;
+			registry[name..":amount"] = function () return "amount", v; end
+			return function (delta)
+				v = v + delta;
+			end;
+		end;
+		rate = function (name)
+			local since, n = time(), 0;
+			registry[name..":rate"] = function ()
+				local t = time();
+				local stats = {
+					rate = n/(t-since);
+					count = n;
+				};
+				since, n = t, 0;
+				return "rate", stats.rate, stats;
+			end;
+			return function ()
+				n = n + 1;
+			end;
+		end;
+		distribution = function (name, unit, type)
+			type = type or "distribution";
+			local events, last_event = {}, 0;
+			local n_actual_events = 0;
+			local since = time();
+
+			registry[name..":"..type] = function ()
+				local new_time = time();
+				local stats = get_distribution_stats(events, n_actual_events, since, new_time, unit);
+				events, last_event = {}, 0;
+				n_actual_events = 0;
+				since = new_time;
+				return type, stats.average, stats;
+			end;
+
+			return function (value)
+				n_actual_events = n_actual_events + 1;
+				if n_actual_events%duration_sample_interval > 0 then
+					last_event = (last_event%duration_max_samples) + 1;
+					events[last_event] = value;
+				end
+			end;
+		end;
+		sizes = function (name)
+			return methods.distribution(name, "bytes", "size");
+		end;
+		times = function (name)
+			local events, last_event = {}, 0;
+			local n_actual_events = 0;
+			local since = time();
+
+			registry[name..":duration"] = function ()
+				local new_time = time();
+				local stats = get_distribution_stats(events, n_actual_events, since, new_time, "seconds");
+				events, last_event = {}, 0;
+				n_actual_events = 0;
+				since = new_time;
+				return "duration", stats.average, stats;
+			end;
+
+			return function ()
+				n_actual_events = n_actual_events + 1;
+				if n_actual_events%duration_sample_interval > 0 then
+					return nop_function;
+				end
+
+				local start_time = time();
+				return function ()
+					local end_time = time();
+					local duration = end_time - start_time;
+					last_event = (last_event%duration_max_samples) + 1;
+					events[last_event] = duration;
+				end
+			end;
+		end;
+
+		get_stats = function ()
+			return registry;
+		end;
+	};
+	return methods;
+end
+
+return {
+	new = new_registry;
+	get_histogram = function (duration, n_buckets)
+		n_buckets = n_buckets or 100;
+		local events, n_events = duration.samples, duration.sample_count;
+		if not (events and n_events) then
+			return nil, "not a valid distribution stat";
+		end
+		local histogram = {};
+
+		for i = 1, 100, 100/n_buckets do
+			histogram[i] = percentile(events, n_events, i);
+		end
+		return histogram;
+	end;
+
+	get_percentile = function (duration, pc)
+		local events, n_events = duration.samples, duration.sample_count;
+		if not (events and n_events) then
+			return nil, "not a valid distribution stat";
+		end
+		return percentile(events, n_events, pc);
+	end;
+}
--- a/util/termcolours.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/termcolours.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
--- a/util/timer.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/timer.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -42,7 +42,7 @@
 			end
 			new_data = {};
 		end
-		
+
 		local next_time = math_huge;
 		for i, d in pairs(data) do
 			local t, callback = d[1], d[2];
--- a/util/uuid.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/uuid.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,50 +1,32 @@
 -- 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 m_random = math.random;
-local tostring = tostring;
-local os_time = os.time;
-local os_clock = os.clock;
-local sha1 = require "util.hashes".sha1;
+local random = require "util.random";
+local random_bytes = random.bytes;
+local hex = require "util.hex".to;
+local m_ceil = math.ceil;
 
-module "uuid"
-
-local last_uniq_time = 0;
-local function uniq_time()
-	local new_uniq_time = os_time();
-	if last_uniq_time >= new_uniq_time then new_uniq_time = last_uniq_time + 1; end
-	last_uniq_time = new_uniq_time;
-	return new_uniq_time;
+local function get_nibbles(n)
+	return hex(random_bytes(m_ceil(n/2))):sub(1, n);
 end
 
-local function new_random(x)
-	return sha1(x..os_clock()..tostring({}), true);
-end
-
-local buffer = new_random(uniq_time());
-local function _seed(x)
-	buffer = new_random(buffer..x);
-end
-local function get_nibbles(n)
-	if #buffer < n then _seed(uniq_time()); end
-	local r = buffer:sub(0, n);
-	buffer = buffer:sub(n+1);
-	return r;
-end
 local function get_twobits()
 	return ("%x"):format(get_nibbles(1):byte() % 4 + 8);
 end
 
-function generate()
+local function generate()
 	-- generate RFC 4122 complaint UUIDs (version 4 - random)
 	return get_nibbles(8).."-"..get_nibbles(4).."-4"..get_nibbles(3).."-"..(get_twobits())..get_nibbles(3).."-"..get_nibbles(12);
 end
-seed = _seed;
 
-return _M;
+return {
+	get_nibbles=get_nibbles;
+	generate = generate ;
+	-- COMPAT
+	seed = random.seed;
+};
--- a/util/x509.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/x509.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -20,11 +20,9 @@
 
 local nameprep = require "util.encodings".stringprep.nameprep;
 local idna_to_ascii = require "util.encodings".idna.to_ascii;
+local base64 = require "util.encodings".base64;
 local log = require "util.logger".init("x509");
-local pairs, ipairs = pairs, ipairs;
 local s_format = string.format;
-local t_insert = table.insert;
-local t_concat = table.concat;
 
 module "x509"
 
@@ -161,7 +159,9 @@
 
 		if sans[oid_xmppaddr] then
 			had_supported_altnames = true
-			if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
+			if service == "_xmpp-client" or service == "_xmpp-server" then
+				if compare_xmppaddr(host, sans[oid_xmppaddr]) then return true end
+			end
 		end
 
 		if sans[oid_dnssrv] then
@@ -212,4 +212,23 @@
 	return false
 end
 
+local pat = "%-%-%-%-%-BEGIN ([A-Z ]+)%-%-%-%-%-\r?\n"..
+"([0-9A-Za-z+/=\r\n]*)\r?\n%-%-%-%-%-END %1%-%-%-%-%-";
+
+function pem2der(pem)
+	local typ, data = pem:match(pat);
+	if typ and data then
+		return base64.decode(data), typ;
+	end
+end
+
+local wrap = ('.'):rep(64);
+local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n"
+
+function der2pem(data, typ)
+	typ = typ and typ:upper() or "CERTIFICATE";
+	data = base64.encode(data);
+	return s_format(envelope, typ, data:gsub(wrap, '%0\n', (#data-1)/64), typ);
+end
+
 return _M;
--- a/util/xml.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/xml.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -26,8 +26,8 @@
 				attr[i] = nil;
 				local ns, nm = k:match(ns_pattern);
 				if nm ~= "" then
-					ns = ns_prefixes[ns]; 
-					if ns then 
+					ns = ns_prefixes[ns];
+					if ns then
 						attr[ns..":"..nm] = attr[k];
 						attr[k] = nil;
 					end
--- a/util/xmppstream.lua	Tue Feb 24 15:19:20 2015 +0000
+++ b/util/xmppstream.lua	Tue Feb 24 15:22:09 2015 +0000
@@ -1,7 +1,7 @@
 -- 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.
 --
@@ -47,22 +47,22 @@
 
 function new_sax_handlers(session, stream_callbacks, cb_handleprogress)
 	local xml_handlers = {};
-	
+
 	local cb_streamopened = stream_callbacks.streamopened;
 	local cb_streamclosed = stream_callbacks.streamclosed;
 	local cb_error = stream_callbacks.error or function(session, e, stanza) error("XML stream error: "..tostring(e)..(stanza and ": "..tostring(stanza) or ""),2); end;
 	local cb_handlestanza = stream_callbacks.handlestanza;
 	cb_handleprogress = cb_handleprogress or dummy_cb;
-	
+
 	local stream_ns = stream_callbacks.stream_ns or xmlns_streams;
 	local stream_tag = stream_callbacks.stream_tag or "stream";
 	if stream_ns ~= "" then
 		stream_tag = stream_ns..ns_separator..stream_tag;
 	end
 	local stream_error_tag = stream_ns..ns_separator..(stream_callbacks.error_tag or "error");
-	
+
 	local stream_default_ns = stream_callbacks.default_ns;
-	
+
 	local stack = {};
 	local chardata, stanza = {};
 	local stanza_size = 0;
@@ -82,7 +82,7 @@
 			attr.xmlns = curr_ns;
 			non_streamns_depth = non_streamns_depth + 1;
 		end
-		
+
 		for i=1,#attr do
 			local k = attr[i];
 			attr[i] = nil;
@@ -92,7 +92,7 @@
 				attr[k] = nil;
 			end
 		end
-		
+
 		if not stanza then --if we are not currently inside a stanza
 			if lxp_supports_bytecount then
 				stanza_size = self:getcurrentbytecount();
@@ -116,7 +116,7 @@
 			if curr_ns == "jabber:client" and name ~= "iq" and name ~= "presence" and name ~= "message" then
 				cb_error(session, "invalid-top-level-element");
 			end
-			
+
 			stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt);
 		else -- we are inside a stanza, so add a tag
 			if lxp_supports_bytecount then
@@ -205,22 +205,22 @@
 			error("Failed to abort parsing");
 		end
 	end
-	
+
 	if lxp_supports_doctype then
 		xml_handlers.StartDoctypeDecl = restricted_handler;
 	end
 	xml_handlers.Comment = restricted_handler;
 	xml_handlers.ProcessingInstruction = restricted_handler;
-	
+
 	local function reset()
 		stanza, chardata, stanza_size = nil, {}, 0;
 		stack = {};
 	end
-	
+
 	local function set_session(stream, new_session)
 		session = new_session;
 	end
-	
+
 	return xml_handlers, { reset = reset, set_session = set_session };
 end
 
@@ -241,6 +241,25 @@
 	local parser = new_parser(handlers, ns_separator, false);
 	local parse = parser.parse;
 
+	function session.open_stream(session, from, to)
+		local send = session.sends2s or session.send;
+
+		local attr = {
+			["xmlns:stream"] = "http://etherx.jabber.org/streams",
+			["xml:lang"] = "en",
+			xmlns = stream_callbacks.default_ns,
+			version = session.version and (session.version > 0 and "1.0" or nil),
+			id = session.streamid,
+			from = from or session.host, to = to,
+		};
+		if session.stream_attrs then
+			session:stream_attrs(from, to, attr)
+		end
+		send("<?xml version='1.0'?>");
+		send(st.stanza("stream:stream", attr):top_tag());
+		return true;
+	end
+
 	return {
 		reset = function ()
 			parser = new_parser(handlers, ns_separator, false);