mod_storage_gdbm: Use require directly instead of util.import (which is not available in prosodyctl, breaks adduser etc)
authorKim Alvefur <zash@zash.se>
Sun, 25 Jan 2015 13:04:02 +0100
changeset 1593 3e4d15ae2133
parent 1592 47fb4f36dacd
child 1594 b1ca6485327d
mod_storage_gdbm: Use require directly instead of util.import (which is not available in prosodyctl, breaks adduser etc)
mod_auth_ccert/mod_auth_ccert.lua
mod_auth_dovecot/auth_dovecot/sasl_dovecot.lib.lua
mod_auth_external/mod_auth_external.lua
mod_auth_ha1/mod_auth_ha1.lua
mod_auth_http_async/mod_auth_http_async.lua
mod_auth_imap/auth_imap/mod_auth_imap.lua
mod_auth_imap/auth_imap/sasl_imap.lib.lua
mod_auth_internal_ng/mod_auth_internal_ng.lua
mod_auth_ldap/mod_auth_ldap.lua
mod_auth_pam/mod_auth_pam.lua
mod_auth_sql/mod_auth_sql.lua
mod_bidi/mod_bidi.lua
mod_block_strangers/mod_block_strangers.lua
mod_candy/mod_candy.lua
mod_candy/www_files/index.html
mod_carbons/mod_carbons.lua
mod_compat_muc_admin/mod_compat_muc_admin.lua
mod_default_bookmarks/mod_default_bookmarks.lua
mod_discoitems/mod_discoitems.lua
mod_extdisco/mod_extdisco.lua
mod_firewall/mod_firewall.lua
mod_http_altconnect/mod_http_altconnect.lua
mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua
mod_http_dir_listing/http_dir_listing/resources/style.css
mod_http_dir_listing/http_dir_listing/resources/template.html
mod_http_index/mod_http_index.lua
mod_ipcheck/mod_ipcheck.lua
mod_limit_auth/mod_limit_auth.lua
mod_mam/mod_mam.lua
mod_mam_muc/mod_mam_muc.lua
mod_manifesto/mod_manifesto.lua
mod_message_logging/mod_message_logging.lua
mod_motd_sequential/mod_motd_sequential.lua
mod_onions/mod_onions.lua
mod_profile/mod_profile.lua
mod_rawdebug/mod_rawdebug.lua
mod_register_redirect/mod_register_redirect.lua
mod_register_web/mod_register_web.lua
mod_s2s_auth_dane/mod_s2s_auth_dane.lua
mod_s2s_auth_fingerprint/mod_s2s_auth_fingerprint.lua
mod_s2s_auth_monkeysphere/mod_s2s_auth_monkeysphere.lua
mod_s2s_keysize_policy/mod_s2s_keysize_policy.lua
mod_s2s_log_certs/mod_s2s_log_certs.lua
mod_smacks/mod_smacks.lua
mod_srvinjection/mod_srvinjection.lua
mod_storage_gdbm/mod_storage_gdbm.lua
mod_storage_internal_ng/json.lib.lua
mod_storage_internal_ng/lua.lib.lua
mod_storage_internal_ng/mod_storage_internal_ng.lua
mod_storage_memory/mod_storage_memory.lua
mod_storage_mongodb/mod_storage_mongodb.lua
mod_storage_muc_log/mod_storage_muc_log.lua
mod_storage_multi/mod_storage_multi.lua
mod_strict_https/mod_strict_https.lua
mod_throttle_presence/mod_throttle_presence.lua
mod_vjud/vcard.lib.lua
mod_webpresence/mod_webpresence.lua
--- a/mod_auth_ccert/mod_auth_ccert.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_ccert/mod_auth_ccert.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -60,7 +60,7 @@
 function get_sasl_handler(session)
 	return new_sasl(module.host, {
 		external = session.secure and function(authz)
-			if not session.secure then
+			if not session.secure or not session.conn:ssl() then
 				-- getpeercertificate() on a TCP connection would be bad, abort!
 				(session.log or log)("error", "How did you manage to select EXTERNAL without TLS?");
 				return nil, false;
--- a/mod_auth_dovecot/auth_dovecot/sasl_dovecot.lib.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_dovecot/auth_dovecot/sasl_dovecot.lib.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -243,6 +243,12 @@
 		end
 	end
 
+	-- If dovecot username formatting is on the username will be on the
+	-- original_user field
+	if data.original_user then
+		data.user = data.original_user
+	end
+
 	if data.user then
 		local handle_domain = self.config.handle_domain;
 		local validate_domain = self.config.validate_domain;
--- a/mod_auth_external/mod_auth_external.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_external/mod_auth_external.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -99,6 +99,7 @@
 	end
 
 	local response, err = send_query(query);
+	module:log("debug", "input: %q, output %q", query, response);
 	if not response then
 		log("warn", "Error while waiting for result from auth process: %s", err or "unknown error");
 	elseif (script_type == "ejabberd" and response == "\0\2\0\0") or
--- a/mod_auth_ha1/mod_auth_ha1.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_ha1/mod_auth_ha1.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -11,11 +11,13 @@
 local nodeprep = require "util.encodings".stringprep.nodeprep;
 local nameprep = require "util.encodings".stringprep.nameprep;
 local md5 = require "util.hashes".md5;
+local from_hex = require"util.hex".from;
 
 local host = module.host;
 
 local auth_filename = module:get_option_string("auth_ha1_file", "auth.txt");
 local auth_data = {};
+local hash_len = #md5("", true);
 
 function reload_auth_data()
 	local f, err = io.open(auth_filename);
@@ -38,10 +40,12 @@
 				module:log("error", "Invalid username on line %d of auth file, skipping", line_number);
 			elseif not realm then
 				module:log("error", "Invalid hostname/realm on line %d of auth file, skipping", line_number);
+			elseif #hash ~= hash_len then
+				module:log("error", "Hash of wrong length on line %d of auth file, skipping", line_number);
 			elseif state ~= "authorized" then
 				not_authorized_count = not_authorized_count + 1;
 			elseif realm == host then
-				auth_data[username] = hash;
+				auth_data[username] = from_hex(hash);
 				imported_count = imported_count + 1;
 			end
 		end
@@ -62,7 +66,7 @@
 function provider.test_password(username, password)
 	module:log("debug", "test password for user %s at host %s, %s", username, host, password);
 
-	local test_hash = md5(username..":"..host..":"..password, true);
+	local test_hash = md5(username..":"..host..":"..password);
 
 	if test_hash == auth_data[username] then
 		return true;
@@ -95,7 +99,10 @@
 	return new_sasl(host, {
 		plain_test = function(sasl, username, password, realm)
 			return usermanager.test_password(username, realm, password), true;
-		end
+		end;
+		["digest-md5"] = function(sasl, username, realm, realm_again, charset)
+			return auth_data[username], true;
+		end;
 	});
 end
 
--- a/mod_auth_http_async/mod_auth_http_async.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_http_async/mod_auth_http_async.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -7,7 +7,6 @@
 -- COPYING file in the source package for more information.
 --
 
-local usermanager = require "core.usermanager";
 local new_sasl = require "util.sasl".new;
 local base64 = require "util.encodings".base64.encode;
 local waiter =require "util.async".waiter;
@@ -66,7 +65,7 @@
 function provider.get_sasl_handler()
 	return new_sasl(host, {
 		plain_test = function(sasl, username, password, realm)
-			return usermanager.test_password(username, realm, password), true;
+			return provider.test_password(username, realm, password), true;
 		end
 	});
 end
--- a/mod_auth_imap/auth_imap/mod_auth_imap.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_imap/auth_imap/mod_auth_imap.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -11,6 +11,7 @@
 local imap_service_realm = module:get_option_string("imap_auth_realm", module:get_option("sasl_realm"));
 local imap_service_name = module:get_option_string("imap_auth_service_name");
 local append_host = module:get_option_boolean("auth_append_host");
+local strip_host = module:get_option_boolean("auth_strip_host");
 
 local verify_certificate = module:get_option_boolean("auth_imap_verify_certificate", true);
 local ssl_params = module:get_option("auth_imap_ssl", {
@@ -28,7 +29,7 @@
 		imap_service_realm or realm,
 		imap_service_name or "xmpp",
 		imap_host, imap_port,
-		ssl_params, append_host
+		ssl_params, append_host, strip_host
 	);
 end
 
--- a/mod_auth_imap/auth_imap/sasl_imap.lib.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_imap/auth_imap/sasl_imap.lib.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -112,7 +112,7 @@
 end
 
 -- create a new SASL object which can be used to authenticate clients
-function _M.new(realm, service_name, host, port, ssl_params, append_host)
+function _M.new(realm, service_name, host, port, ssl_params, append_host, strip_host)
 	log("debug", "new(%q, %q, %q, %d)", realm or "", service_name or "", host or "", port or 0);
 	local sasl_i = {
 		realm = realm;
@@ -121,13 +121,14 @@
 		_port = port;
 		_ssl_params = ssl_params;
 		_append_host = append_host;
+		_strip_host = strip_host;
 	};
 
 	local conn, mechs = connect(host, port, ssl_params);
 	if not conn then
 		return nil, "Socket connection failure";
 	end
-	if append_host then
+	if append_host or strip_host then
 		mechs = { PLAIN = mechs.PLAIN };
 	end
 	sasl_i.conn, sasl_i.mechs = conn, mechs;
@@ -141,7 +142,7 @@
 		self.conn = nil;
 	end
 	log("debug", "method:clean_clone()");
-	return _M.new(self.realm, self.service_name, self._host, self._port, self._ssl_params, self._append_host)
+	return _M.new(self.realm, self.service_name, self._host, self._port, self._ssl_params, self._append_host, self._strip_host)
 end
 
 -- get a list of possible SASL mechanisms to use
@@ -177,8 +178,12 @@
 function method:process(message)
 	local username = mitm[self.selected](message);
 	if username then self.username = username; end
-	if self._append_host and self.selected == "PLAIN" then
-		message = message:gsub("^([^%z]*%z[^%z]+)(%z[^%z]+)$", "%1@"..self.realm.."%2");
+	if self.selected == "PLAIN" then
+		if self._append_host then
+			message = message:gsub("^%Z*(%z[^%z@]+)@?%Z*(%z%Z+)$", "%1@"..self.realm.."%2");
+		elseif self._strip_host then
+			message = message:gsub("^(%Z*%z[^@%z]+)@%Z+", "%1");
+		end
 	end
 	log("debug", "method:process(%d bytes): %q", #message, message:gsub("%z", "."));
 	local ok, err = self.conn:send(b64(message).."\n");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_auth_internal_ng/mod_auth_internal_ng.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -0,0 +1,147 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-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.
+--
+
+local new_sasl = require "util.sasl".new;
+local random_bytes = require"util.random".bytes;
+local rfc5803 = require"util.rfc5803";
+local scram = require "util.sasl.scram";
+local hex = require"util.hex";
+
+local mode = module:get_option_string("store_credentials", "hashed");
+local default_i = module:get_option_number("default_iteration_count", 2^12);
+
+local accounts = module:open_store("accounts");
+
+local function hashify(account)
+	local i = default_i;
+	local password = account.password;
+
+	if account.hashedPassword then
+		if not password then return end -- Already hashed
+		local _, old_i = rfc5803.unpack(account.hashedPassword);
+		if old_i > i then i = old_i; end
+	end
+	local salt = random_bytes(16);
+	local ok, stored_key, server_key = scram.getAuthenticationDatabaseSHA1(password, salt, i);
+	if not ok then return nil, stored_key; end
+	account.hashedPassword = rfc5803.pack("SHA-1", i, salt, stored_key, server_key);
+	account.password = nil;
+	account.iteration_count, account.salt = nil, nil;
+	account.stored_key, account.server_key = nil, nil;
+	return account;
+end
+
+local function get_scram_hash(account)
+	if account.hashedPassword then
+		return rfc5803.unpack(account.hashedPassword);
+	elseif account.stored_key then
+		return "SHA-1", account.iteration_count, account.salt,
+			hex.from(account.stored_key), hex.from(account.server_key);
+	end
+end
+
+function user_exists(username)
+	local account, err = accounts:get(username);
+	if not account then return account, err; end
+	return next(account) ~= nil;
+end
+
+function create_user(username, password)
+	local account = { password = password };
+	if mode == "hashed" then
+		hashify(account);
+	end
+	return accounts:set(username, account);
+end
+
+function delete_user(username)
+	return accounts:set(username, nil);
+end
+
+function test_password(username, password)
+	local account, err = accounts:get(username);
+	if not account then return account, err; end
+	if account.password then
+		return password == account.password;
+	end
+	local hash, i, salt, our_stored_key, our_server_key = get_scram_hash(account);
+	local ok, stored_key, server_key = scram.getAuthenticationDatabaseSHA1(password, salt, i);
+	if not ok then return ok, stored_key; end
+	ok = hash == "SHA-1" and stored_key == our_stored_key and server_key == our_server_key;
+	if ok and mode == "unhash" then
+		account.password, account.hashedPassword = password;
+		accounts:set(username, account);
+	end
+	return ok;
+end
+
+local sasl_profile = {};
+
+function get_sasl_handler()
+	return new_sasl(module.host, sasl_profile);
+end
+
+function set_password(username, password)
+	local account, err = accounts:get(username);
+	if not account then account = {}; end
+	account.password = password;
+	if mode == "hashed" then
+		account, err = hashify(account);
+	end
+	if not account then return account, err; end
+	return accounts:set(username, account);
+end
+
+if mode == "plain" then
+	function get_password(username)
+		local account, err = accounts:get(username);
+		if not account then return nil, err; end
+		return account.password;
+	end
+
+	function sasl_profile:plain(username)
+		return get_password(username), true;
+	end
+elseif mode == "unhash" then
+	function sasl_profile:plain_test(username, password)
+		local ok = test_password(username, password);
+		if ok then
+			set_password(username, password);
+		end
+		return ok, true;
+	end
+elseif mode == "hashed" then
+	function sasl_profile:plain_test(username, password)
+		return test_password(username, password), true;
+	end
+
+	function sasl_profile:scram_sha_1(username)
+		local account, err = accounts:get(username);
+		if not account then return account, err; end
+		if not account.hashedPassword and hashify(account) then
+			accounts:set(username, account);
+		end
+		local hash, i, salt, stored_key, server_key = get_scram_hash(account);
+		if hash == "SHA-1" then
+			return stored_key, server_key, i, salt, true;
+		end
+	end
+else
+	module:log("error", "Unsupported store_credentials mode %q", mode);
+	return;
+end
+
+if accounts.users then
+	function users()
+		return accounts:users();
+	end
+end
+
+module:provides "auth";
+
--- a/mod_auth_ldap/mod_auth_ldap.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_ldap/mod_auth_ldap.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -16,12 +16,42 @@
 local host = ldap_filter_escape(module:get_option_string("realm", module.host));
 
 -- Initiate connection
-local ld = assert(lualdap.open_simple(ldap_server, ldap_rootdn, ldap_password, ldap_tls));
-module.unload = function() ld:close(); end
+local ld = nil;
+module.unload = function() if ld then pcall(ld, ld.close); end end
+
+function ldap_search_once(args)
+	if ld == nil then
+		local err;
+		ld, err = lualdap.open_simple(ldap_server, ldap_rootdn, ldap_password, ldap_tls);
+		if not ld then return nil, err, "reconnect"; end
+	end
+
+	local success, iterator, invariant, initial = pcall(ld.search, ld, args);
+	if not success then ld = nil; return nil, iterator, "search"; end
+
+	local success, dn, attr = pcall(iterator, invariant, initial);
+	if not success then ld = nil; return success, dn, "iter"; end
+	
+	return dn, attr, "return";
+end
+
+function ldap_search(args, retry_count)
+	local dn, attr, where;
+	for i=1,1+retry_count do
+		dn, attr, where = ldap_search_once(args);
+		if dn or not(attr) then break; end -- nothing or something found
+		module:log("warn", "LDAP: %s %s (in %s)", tostring(dn), tostring(attr), where);
+		-- otherwise retry
+	end
+	if not dn and attr then
+		module:log("error", "LDAP: %s", tostring(attr));
+	end
+	return dn, attr;
+end
 
 local function get_user(username)
 	module:log("debug", "get_user(%q)", username);
-	for dn, attr in ld:search({
+	return ldap_search({
 		base = ldap_base;
 		scope = ldap_scope;
 		sizelimit = 1;
@@ -29,7 +59,7 @@
 			user = ldap_filter_escape(username);
 			host = host;
 		});
-	}) do return dn, attr; end
+	}, 2);
 end
 
 local provider = {};
@@ -39,7 +69,9 @@
 end
 
 function provider.user_exists(username)
-	return not not get_user(username);
+	local dn, attr = get_user(username);
+	if not dn and attr then return nil, attr; end
+	return not not dn;
 end
 
 function provider.set_password(username, password)
@@ -76,8 +108,8 @@
 	end
 
 	function provider.test_password(username, password)
-		local dn = get_user(username);
-		if not dn then return end
+		local dn, attr = get_user(username);
+		if not dn then return nil, attr; end
 		return test_password(dn, password)
 	end
 
@@ -88,6 +120,30 @@
 			end
 		});
 	end
+elseif ldap_mode == "directbind" then
+	local function test_password(userdn, password)
+		return not not lualdap.open_simple(ldap_server, userdn, password, ldap_tls);
+	end
+
+	function provider.user_exists(username)
+		return true;
+	end
+
+	function provider.test_password(username, password)
+		local dn = ldap_filter:gsub("%$(%a+)", {
+			user = ldap_filter_escape(username);
+			host = host;
+		});
+		return test_password(dn, password);
+	end
+
+	function provider.get_sasl_handler()
+		return new_sasl(module.host, {
+			plain_test = function(sasl, username, password)
+				return provider.test_password(username, password), true;
+			end
+		});
+	end
 else
 	module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode));
 end
--- a/mod_auth_pam/mod_auth_pam.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_pam/mod_auth_pam.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -4,14 +4,21 @@
 -- Requires https://github.com/devurandom/lua-pam
 -- and LuaPosix
 
-local posix = require "posix";
+local have_posix, posix = pcall(require, "posix");
 local pam = require "pam";
 local new_sasl = require "util.sasl".new;
 
-function user_exists(username)
-	return not not posix.getpasswd(username);
+if have_posix then
+	function user_exists(username)
+		return not not posix.getpasswd(username);
+	end
+else
+	function user_exists()
+		return true;
+	end
 end
 
+
 function test_password(username, password)
 	local h, err = pam.start("xmpp", username, {
 		function (t)
@@ -21,15 +28,14 @@
 		end
 	});
 	if h and h:authenticate() and h:endx(pam.SUCCESS) then
-		return user_exists(username), true;
+		return user_exists(username);
 	end
-	return nil, true;
 end
 
 function get_sasl_handler()
 	return new_sasl(module.host, {
 		plain_test = function(sasl, ...)
-			return test_password(...)
+			return test_password(...), true;
 		end
 	});
 end
--- a/mod_auth_sql/mod_auth_sql.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_auth_sql/mod_auth_sql.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -79,6 +79,13 @@
 	end
 end
 
+local function set_password(username, password)
+	return getsql("UPDATE `authreg` SET `password` = ? WHERE `username`=? AND `realm`=?", username, module.host);
+end
+
+local function create_user(username, password)
+	return getsql("INSERT INTO `authreg` (`username`, `realm`, `password`) VALUES (?, ?, ?)", username, module.host, password);
+end
 
 provider = {};
 
@@ -89,13 +96,13 @@
 	return get_password(username);
 end
 function provider.set_password(username, password)
-	return nil, "Setting password is not supported.";
+	return set_password(username, password);
 end
 function provider.user_exists(username)
 	return get_password(username) and true;
 end
 function provider.create_user(username, password)
-	return nil, "Account creation/modification not supported.";
+	return create_user(username, password);
 end
 function provider.get_sasl_handler()
 	local profile = {
--- a/mod_bidi/mod_bidi.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_bidi/mod_bidi.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -14,7 +14,10 @@
 local xmlns_bidi = "urn:xmpp:bidi";
 local secure_only = module:get_option_boolean("secure_bidi_only", true);
 local disable_bidi_for = module:get_option_set("no_bidi_with", { });
-local bidi_sessions = module:shared"sessions-cache";
+local bidi_sessions = module:shared"sessions";
+-- if not getmetatable(bidi_sessions) then
+	-- setmetatable(bidi_sessions, { __mode = "kv" });
+-- end
 
 local function handleerr(err) log("error", "Traceback[s2s]: %s: %s", tostring(err), traceback()); end
 local function handlestanza(session, stanza)
@@ -30,7 +33,7 @@
 local function new_bidi(origin)
 	if origin.type == "s2sin" then -- then we create an "outgoing" bidirectional session
 		local conflicting_session = hosts[origin.to_host].s2sout[origin.from_host]
-		if conflicting_session then
+		if false and conflicting_session then
 			conflicting_session.log("info", "We already have an outgoing connection to %s, closing it...", origin.from_host);
 			conflicting_session:close{ condition = "conflict", text = "Replaced by bidirectional stream" }
 		end
--- a/mod_block_strangers/mod_block_strangers.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_block_strangers/mod_block_strangers.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,20 +1,58 @@
+module:depends("adhoc");
 
 local jid_split = require "util.jid".split;
 local jid_bare = require "util.jid".bare;
 local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
+local adhoc_new = module:require "adhoc".new;
+local bare_sessions = bare_sessions
 
-function check_subscribed(event)
+local storage = module:open_store();
+
+local function filter_stanza(event)
 	local stanza = event.stanza;
 	local to_user, to_host, to_resource = jid_split(stanza.attr.to);
-	local from_jid = jid_bare(stanza.attr.from);
-	if to_user and not is_contact_subscribed(to_user, to_host, from_jid) then
-		if to_resource and stanza.attr.type == "groupchat" then
-			return nil; -- Pass through
+	if bare_sessions[to_user.."@"..to_host].block_strangers then
+		local from_jid = jid_bare(stanza.attr.from);
+		if to_user and not is_contact_subscribed(to_user, to_host, from_jid) then
+			if to_resource and stanza.attr.type == "groupchat" then
+				return nil; -- Pass through
+			end
+			return true; -- Drop stanza
 		end
-		return true; -- Drop stanza
 	end
 end
 
-module:hook("message/bare", check_subscribed, 200);
-module:hook("message/full", check_subscribed, 200);
-module:hook("iq/full", check_subscribed, 200);
+local function load_blocking_status(event)
+	local origin = event.origin;
+	local jid = jid_bare(origin.full_jid);
+	bare_sessions[jid].block_strangers = not not storage:get(username);
+end
+
+module:hook("message/bare", filter_stanza, 200);
+module:hook("message/full", filter_stanza, 200);
+module:hook("iq/full", filter_stanza, 200);
+
+module:hook("presence/bare", load_blocking_status);
+
+
+-- Expose blocking/unblocking through ad-hoc commands.
+
+local function toggle_global_blocking(self, data)
+	local username, host = jid_split(data.from);
+	local enabling = (self.node == "mod_block_strangers#enable");
+	local noun = enabling and "blocked" or "unblocked";
+	storage:set(username, enabling and {enabled=true} or {});
+	bare_sessions[username.."@"..host].block_strangers = enabling;
+	return { info = "Strangers successfully "..noun, status = "completed" };
+end
+
+local function get_blocking_status(self, data)
+	local jid = jid_bare(data.from);
+	local enabled = bare_sessions[jid].block_strangers;
+	local noun = enabled and "blocked" or "unblocked";
+	return { info = "Strangers are currently "..noun, status = "completed" };
+end
+
+module:add_item("adhoc", adhoc_new("Block strangers", "mod_block_strangers#enable", toggle_global_blocking));
+module:add_item("adhoc", adhoc_new("Unblock strangers", "mod_block_strangers#disable", toggle_global_blocking));
+module:add_item("adhoc", adhoc_new("Get strangers block status", "mod_block_strangers#status", get_blocking_status));
--- a/mod_candy/mod_candy.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_candy/mod_candy.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -23,10 +23,6 @@
 					}));
 		end;
 		["GET /*"] = serve(module:get_directory().."/www_files");
-		GET = function(event)
-			event.response.headers.location = event.request.path.."/";
-			return 301;
-		end;
 	}
 });
 
--- a/mod_candy/www_files/index.html	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_candy/www_files/index.html	Sun Jan 25 13:04:02 2015 +0100
@@ -13,7 +13,7 @@
 	<script type="text/javascript">
 		$(document).ready(function() {
 			Candy.init(Prosody.bosh_path, {
-				core: { debug: false },
+				core: { debug: false, autojoin: [ "test@muc.carcharodon.zash.se" ] },
 				view: { resources: 'res/' }
 			});
 
--- a/mod_carbons/mod_carbons.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_carbons/mod_carbons.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -16,6 +16,7 @@
 	local state = stanza.tags[1].attr.mode or 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;
+	module:fire_event("session-state", { session = origin, property = "want_carbons" });
 	return origin.send(st.reply(stanza));
 end
 module:hook("iq-set/self/"..xmlns_carbons..":disable", toggle_carbons);
@@ -62,17 +63,7 @@
 		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
+	if 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
--- a/mod_compat_muc_admin/mod_compat_muc_admin.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_compat_muc_admin/mod_compat_muc_admin.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -44,7 +44,7 @@
 	local tag = stanza;
 	for _, name in ipairs(path) do
 		if type(tag) ~= 'table' then return; end
-		tag = tag:child_with_name(name);
+		tag = tag:get_child(name);
 	end
 	if tag and getText then tag = table.concat(tag); end
 	return tag;
--- a/mod_default_bookmarks/mod_default_bookmarks.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_default_bookmarks/mod_default_bookmarks.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -13,37 +13,34 @@
 local dm_load = require "util.datamanager".load
 local jid_split = require "util.jid".split
 
-module:hook("iq/self/jabber:iq:private:query", function(event)
+local private_bookmarks_ns = "storage:storage:bookmarks";
+
+local bookmarks = module:get_option("default_bookmarks");
+
+module:hook("iq-get/self/jabber:iq:private:query", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	local typ = stanza.attr.type;
 	local from = stanza.attr.from;
-	local query = stanza.tags[1];
-	if #query.tags == 1 and typ == "get" then
-		local tag = query.tags[1];
-		local key = tag.name..":"..tag.attr.xmlns;
-		if key == "storage:storage:bookmarks" then
-			local data, err = dm_load(origin.username, origin.host, "private");
-			if not(data and data[key]) then
-				local bookmarks = module:get_option("default_bookmarks");
-				if bookmarks and #bookmarks > 0 then
-					local reply = st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"})
-						:tag("storage", { xmlns = "storage:bookmarks" });
-					local nick = jid_split(from);
-					for i=1,#bookmarks do
-						local bookmark = bookmarks[i];
-						if type(bookmark) ~= "table" then -- assume it's only a jid
-							bookmark = { jid = bookmark, name = jid_split(bookmark) };
-						end
-						reply:tag("conference", {
-							jid = bookmark.jid,
-							name = bookmark.name,
-							autojoin = "1",
-						}):tag("nick"):text(nick):up():up();
-					end
-					origin.send(reply);
-					return true;
-				end
-			end
+	if not stanza.tags[1]:get_child("storage", "storage:bookmarks") then return end
+	local data, err = dm_load(origin.username, origin.host, "private");
+	if data and data[private_bookmarks_ns] then return end
+
+	local reply = st.reply(stanza):tag("query", {xmlns = "jabber:iq:private"})
+		:tag("storage", { xmlns = "storage:bookmarks" });
+
+	local nick = jid_split(from);
+
+	local bookmark;
+	for i=1,#bookmarks do
+		bookmark = bookmarks[i];
+		if type(bookmark) ~= "table" then -- assume it's only a jid
+			bookmark = { jid = bookmark, name = jid_split(bookmark) };
 		end
+		reply:tag("conference", {
+			jid = bookmark.jid,
+			name = bookmark.name,
+			autojoin = "1",
+		}):tag("nick"):text(nick):up():up();
 	end
+	origin.send(reply);
+	return true;
 end, 1);
--- a/mod_discoitems/mod_discoitems.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_discoitems/mod_discoitems.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,25 +1,25 @@
--- mod_discoitems.lua
---
--- In the config, you can add:
---
--- disco_items = {
---  {"proxy.eu.jabber.org", "Jabber.org SOCKS5 service"};
---  {"conference.jabber.org", "The Jabber.org MUC"};
--- };
---
-
-local st = require "util.stanza";
-
-local result_query = st.stanza("query", {xmlns="http://jabber.org/protocol/disco#items"});
-for _, item in ipairs(module:get_option("disco_items") or {}) do
-	result_query:tag("item", {jid=item[1], name=item[2]}):up();
-end
-
-module:hook('iq/host/http://jabber.org/protocol/disco#items:query', function(event)
-	local stanza = event.stanza;
-	local query = stanza.tags[1];
-	if stanza.attr.type == 'get' and not query.attr.node then
-		event.origin.send(st.reply(stanza):add_child(result_query));
-		return true;
-	end
-end, 100);
+-- mod_discoitems.lua
+--
+-- In the config, you can add:
+--
+-- disco_items = {
+--  {"proxy.eu.jabber.org", "Jabber.org SOCKS5 service"};
+--  {"conference.jabber.org", "The Jabber.org MUC"};
+-- };
+--
+
+local st = require "util.stanza";
+
+local result_query = st.stanza("query", {xmlns="http://jabber.org/protocol/disco#items"});
+for _, item in ipairs(module:get_option("disco_items") or {}) do
+	result_query:tag("item", {jid=item[1], name=item[2]}):up();
+end
+
+module:hook('iq/host/http://jabber.org/protocol/disco#items:query', function(event)
+	local stanza = event.stanza;
+	local query = stanza.tags[1];
+	if stanza.attr.type == 'get' and not query.attr.node then
+		event.origin.send(st.reply(stanza):add_child(result_query));
+		return true;
+	end
+end, 100);
--- a/mod_extdisco/mod_extdisco.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_extdisco/mod_extdisco.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,20 +1,28 @@
+local moduleapi = require"core.moduleapi";
 local st = require "util.stanza";
-
-local services = module:get_option("external_services");
+local array = require"util.array";
 
 local xmlns_extdisco = "urn:xmpp:extdisco:1";
 
 module:add_feature(xmlns_extdisco);
 
+function moduleapi:add_external_service(info)
+	return self:add_item("external_service", info);
+end
+
 module:hook("iq-get/host/"..xmlns_extdisco..":services", function (event)
 	local origin, stanza = event.origin, event.stanza;
 	local service = stanza:get_child("service", xmlns_extdisco);
 	local service_type = service and service.attr.type;
-	local reply = st.reply(stanza);
-	for host, service_info in pairs(services) do
+	local reply = st.reply(stanza):tag("services", {xmlns = xmlns_extdisco});
+	local services = array():append(module:get_host_items("external_services"));
+	local event = { services = services, origin = origin, type = service_type };
+	module:fire_event("external-service-discovery", event);
+	for i = 1, #services do
+		local service_info = services[i];
 		if not(service_type) or service_info.type == service_type then
 			reply:tag("service", {
-				host = host;
+				host = service_info.host;
 				port = service_info.port;
 				transport = service_info.transport;
 				type = service_info.type;
@@ -27,6 +35,20 @@
 	return true;
 end);
 
+function module.load()
+	local services = module:get_option("external_services");
+	for host, service_info in pairs(services) do
+		module:add_external_service {
+			host = host,
+			port = service_info.port;
+			transport = service_info.transport;
+			type = service_info.type;
+			username = service_info.username;
+			password = service_info.password;
+		}
+	end
+end
+
 module:hook("iq-get/host/"..xmlns_extdisco..":credentials", function (event)
 	local origin, stanza = event.origin, event.stanza;
 	local credentials = stanza:get_child("credentials", xmlns_extdisco);
@@ -35,7 +57,7 @@
 		origin.send(st.error_reply(stanza, "cancel", "bad-request", "No host specified"));
 		return true;
 	end
-	local service_info = services[host];
+	local service_info = module:fire_event("external-services-credentials") or services[host];
 	if not service_info then
 		origin.send(st.error_reply(stanza, "cancel", "item-not-found", "No such service known"));
 		return true;
@@ -50,3 +72,4 @@
 	origin.send(reply);
 	return true;
 end);
+
--- a/mod_firewall/mod_firewall.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_firewall/mod_firewall.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -27,6 +27,10 @@
 		type = "event"; "route/remote";
 		priority = 0.1;
 	};
+	send_remote = { -- FIXME name
+		type = "filter"; "s2sout";
+		priority = 0.1;
+	};
 };
 
 local function idsafe(name)
@@ -372,9 +376,13 @@
 					module:log("error", "Compilation error for %s: %s", script, err);
 				else
 					local chain_definition = chains[chain];
-					if chain_definition and chain_definition.type == "event" then
-						for _, event_name in ipairs(chain_definition) do
-							module:hook(event_name, handler, chain_definition.priority);
+					if chain_definition then
+						if chain_definition.type == "event" then
+							for _, event_name in ipairs(chain_definition) do
+								module:hook(event_name, handler, chain_definition.priority);
+							end
+						elseif chain_definition.type == "filter" then
+							-- TODO
 						end
 					elseif not chain:match("^user/") then
 						module:log("warn", "Unknown chain %q", chain);
--- a/mod_http_altconnect/mod_http_altconnect.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_http_altconnect/mod_http_altconnect.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -6,16 +6,20 @@
 local json = require"util.json";
 local st = require"util.stanza";
 local array = require"util.array";
+local it = require"util.iterators";
 
 local host_modules = hosts[module.host].modules;
 
 local function get_supported()
-	local uris = array();
-	if host_modules["bosh"] then
-		uris:push({ rel = "urn:xmpp:alt-connections:xbosh", href = module:http_url("bosh", "/http-bind") });
-	end
-	if host_modules["websocket"] then
-		uris:push({ rel = "urn:xmpp:alt-connections:websocket", href = module:http_url("websocket", "xmpp-websocket"):gsub("^http", "ws") });
+	local uris = array(it.values(module:get_host_items("alt-conn-method")));
+	if #uris == 0 then
+		-- COMPAT for with before item array was added
+		if host_modules["bosh"] then
+			uris:push({ rel = "urn:xmpp:alt-connections:xbosh", href = module:http_url("bosh", "/http-bind") });
+		end
+		if host_modules["websocket"] then
+			uris:push({ rel = "urn:xmpp:alt-connections:websocket", href = module:http_url("websocket", "xmpp-websocket"):gsub("^http", "ws") });
+		end
 	end
 	return uris;
 end
@@ -50,6 +54,16 @@
 	end
 end;
 
+function module.load()
+	local methods = get_supported()
+	if #methods > 0 then
+		module:log("info", "To advertise alternative XMPP connection methods via dns, add these TXT records:");
+		for i, method in ipairs(methods) do
+			module:log("info", "_xmppconnect IN TXT \"%s=%s\"", method.rel:gsub("^urn:xmpp:alt%-connections:", "_xmpp-client-"), method.href);
+		end
+	end
+end
+
 module:provides("http", {
 	default_path = "/.well-known";
 	route = {
--- a/mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_http_dir_listing/http_dir_listing/mod_http_dir_listing.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -58,6 +58,7 @@
 end
 
 module:hook_object_event(server, "directory-index", function (event)
+	module:log("debug", "generate directory index for %s (%s)", event.path, event.full_path);
 	local ok, data = pcall(generate_directory_index, event.path, event.full_path);
 	if ok then return data end
 	module:log("warn", data);
--- a/mod_http_dir_listing/http_dir_listing/resources/style.css	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_http_dir_listing/http_dir_listing/resources/style.css	Sun Jan 25 13:04:02 2015 +0100
@@ -5,6 +5,12 @@
 a:link:hover,a:visited:hover{color:#3465a4;}
 .filelist{background-color:white;padding:1em;list-style-position:inside;-moz-column-width:20em;-webkit-column-width:20em;-ms-column-width:20em;column-width:20em;}
 .file{list-style-image:url(text-x-generic.png);}
+.file.image{list-style-image:url(image-x-generic.png);}
+.file.video{list-style-image:url(video-x-generic.png);}
+.file.audio{list-style-image:url(audio-x-generic.png);}
+.file.vcf{list-style-image:url(x-office-address-book.png);}
+.file.text.html{list-style-image:url(text-html.png);}
+.file.application{list-style-image:url(application-x-executable.png);}
 .directory{list-style-image:url(folder.png);}
 .parent{list-style-image:url(user-home.png);}
 footer{margin-top:1ex;font-size:smaller;color:#babdb6;}
--- a/mod_http_dir_listing/http_dir_listing/resources/template.html	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_http_dir_listing/http_dir_listing/resources/template.html	Sun Jan 25 13:04:02 2015 +0100
@@ -6,9 +6,9 @@
   </head>
   <body>
     <h1>Index of {path}</h1>
-
-		{filelist}
-
+      <article>
+        {filelist}
+      </article>
     <footer>{footer}</footer>
   </body>
 </html>
--- a/mod_http_index/mod_http_index.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_http_index/mod_http_index.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -45,12 +45,6 @@
 .content{background-color:white;padding:1em;list-style-position:inside;}
 nav{font-size:large;margin:1ex 1ex;clear:both;line-height:1.5em;}
 nav a{padding: 1ex;text-decoration:none;}
-nav a[rel="up"]{font-size:smaller;}
-nav a[rel="prev"]{float:left;}
-nav a[rel="next"]{float:right;}
-nav a[rel="next::after"]{content:" →";}
-nav a[rel="prev::before"]{content:"← ";}
-nav a:empty::after,nav a:empty::before{content:""}
 @media screen and (min-width: 460px) {
 nav{font-size:x-large;margin:1ex 1em;}
 }
@@ -60,12 +54,7 @@
 li{list-style:none;}
 hr{visibility:hidden;clear:both;}
 br{clear:both;}
-li time{float:right;font-size:small;opacity:0.2;}
 li:hover time{opacity:1;}
-.room-list .description{font-size:smaller;}
-q.body::before,q.body::after{content:"";}
-.presence .verb{font-style:normal;color:#30c030;}
-.presence.unavailable .verb{color:#c03030;}
 </style>
 </head>
 <body>
--- a/mod_ipcheck/mod_ipcheck.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_ipcheck/mod_ipcheck.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -6,41 +6,37 @@
 
 module:add_feature("urn:xmpp:sic:0");
 
-module:hook("iq/bare/urn:xmpp:sic:0:ip", function(event)
+module:hook("iq-get/bare/urn:xmpp:sic:0:ip", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	if stanza.attr.type == "get" then
-		if stanza.attr.to then
-			origin.send(st.error_reply(stanza, "auth", "forbidden", "You can only ask about your own IP address"));
-		elseif origin.ip then
-			origin.send(st.reply(stanza):tag("ip", {xmlns='urn:xmpp:sic:0'}):text(origin.ip));
-		else
-			-- IP addresses should normally be available, but in case they are not
-			origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "IP address for this session is not available"));
-		end
-		return true;
+	if stanza.attr.to then
+		origin.send(st.error_reply(stanza, "auth", "forbidden", "You can only ask about your own IP address"));
+	elseif origin.ip then
+		origin.send(st.reply(stanza):tag("ip", {xmlns='urn:xmpp:sic:0'}):text(origin.ip));
+	else
+		-- IP addresses should normally be available, but in case they are not
+		origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "IP address for this session is not available"));
 	end
+	return true;
 end);
 
 module:add_feature("urn:xmpp:sic:1");
 
-module:hook("iq/bare/urn:xmpp:sic:1:address", function(event)
+module:hook("iq-get/bare/urn:xmpp:sic:1:address", function(event)
 	local origin, stanza = event.origin, event.stanza;
-	if stanza.attr.type == "get" then
-		if stanza.attr.to then
-			origin.send(st.error_reply(stanza, "auth", "forbidden", "You can only ask about your own IP address"));
-		elseif origin.ip then
-			local reply = st.reply(stanza):tag("address", {xmlns='urn:xmpp:sic:0'})
-				:tag("ip"):text(origin.ip):up()
-			if origin.conn and origin.conn.port then -- server_event
-				reply:tag("port"):text(tostring(origin.conn:port()))
-			elseif origin.conn and origin.conn.clientport then -- server_select
-				reply:tag("port"):text(tostring(origin.conn:clientport()))
-			end
-			origin.send(reply);
-		else
-			-- IP addresses should normally be available, but in case they are not
-			origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "IP address for this session is not available"));
+	if stanza.attr.to then
+		origin.send(st.error_reply(stanza, "auth", "forbidden", "You can only ask about your own IP address"));
+	elseif origin.ip then
+		local reply = st.reply(stanza):tag("address", {xmlns='urn:xmpp:sic:0'})
+		:tag("ip"):text(origin.ip):up()
+		if origin.conn and origin.conn.port then -- server_event
+			reply:tag("port"):text(tostring(origin.conn:port()))
+		elseif origin.conn and origin.conn.clientport then -- server_select
+			reply:tag("port"):text(tostring(origin.conn:clientport()))
 		end
-		return true;
+		origin.send(reply);
+	else
+		-- IP addresses should normally be available, but in case they are not
+		origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "IP address for this session is not available"));
 	end
+	return true;
 end);
--- a/mod_limit_auth/mod_limit_auth.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_limit_auth/mod_limit_auth.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -7,7 +7,7 @@
 local max = math.max(module:get_option_number(module.name.."_max", 5), 1);
 
 local tarpit_delay = module:get_option_number(module.name.."_tarpit_delay", nil);
-if tarpit_delay then
+if tarpit_delay and tarpit_delay > 0 then
 	local waiter = require "util.async".waiter;
 	local delay = tarpit_delay;
 	function tarpit_delay()
@@ -46,4 +46,12 @@
 	get_throttle(event.session.ip):poll(1);
 end);
 
--- TODO remove old throttles after some time
+module:add_timer(144, function (t)
+	t = t - 86400;
+	for ip, throttle in pairs(throttles) do
+		if throttle.t < t then
+			throttles[ip] = nil;
+		end
+	end
+	return 144;
+end);
--- a/mod_mam/mod_mam.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_mam/mod_mam.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -3,7 +3,7 @@
 --
 -- This file is MIT/X11 licensed.
 
-local xmlns_mam     = "urn:xmpp:mam:0";
+local xmlns_mam     = "urn:xmpp:mam:0"; -- Version 0.3
 local xmlns_delay   = "urn:xmpp:delay";
 local xmlns_forward = "urn:xmpp:forward:0";
 
@@ -35,6 +35,8 @@
 	global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy);
 end
 
+local measure_query_time = module:measure("query", "times");
+
 local archive_store = "archive2";
 local archive = module:open_store(archive_store, "archive");
 if not archive or archive.name == "null" then
@@ -115,6 +117,7 @@
 	local before, after = qset and qset.before, qset and qset.after;
 	if type(before) ~= "string" then before = nil; end
 
+	local query_completed = measure_query_time();
 
 	-- Load all the data!
 	local data, err = archive:find(origin.username, {
@@ -131,7 +134,7 @@
 	end
 	local count = err;
 
-	origin.send(st.reply(stanza))
+	origin.send(st.reply(stanza)); -- Remove in next MAM version
 	local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
 
 	-- Wrap it in stuff and deliver
@@ -156,11 +159,18 @@
 	-- That's all folks!
 	module:log("debug", "Archive query %s completed", tostring(qid));
 
+	query_completed();
+
 	if reverse then first, last = last, first; end
 	return origin.send(st.message(msg_reply_attr)
 		:tag("fin", { xmlns = xmlns_mam, queryid = qid })
 			:add_child(rsm.generate {
 				first = first, last = last, count = count }));
+	--[[ Next MAM version
+	return origin.send(st.reply(stanza)
+		:query(xmlns_mam):add_child(rsm.generate {
+			first = first, last = last, count = count }));
+	--]]
 end);
 
 local function has_in_roster(user, who)
@@ -240,3 +250,101 @@
 
 module:add_feature(xmlns_mam);
 
+
+module:depends"adhoc";
+local dataforms_new = require "util.dataforms".new;
+local jid_split = require "util.jid".split;
+local t_insert = table.insert;
+local prefs = module:require"mod_mam/mamprefs";
+local set_prefs, get_prefs = prefs.set, prefs.get;
+
+local mam_prefs_form = dataforms_new{
+	title = "Archive preferences";
+	--instructions = "";
+	{
+		name = "default",
+		label = "Default storage policy",
+		type = "list-single",
+		value = {
+			{ value = "always", label = "Always", default = global_default_policy == true },
+			{ value = "never", label = "Never", default = global_default_policy == false },
+			{ value = "roster", label = "Roster", default = global_default_policy == "roster" },
+		},
+	};
+	{
+		name = "always",
+		label = "Always store messages to/from",
+		type = "jid-multi"
+	};
+	{
+		name = "never",
+		label = "Never store messages to/from",
+		type = "jid-multi"
+	};
+};
+
+local host = module.host;
+
+local default_attrs = {
+	always = true, [true] = "always",
+	never = false, [false] = "never",
+	roster = "roster",
+}
+
+local function mam_prefs_handler(self, data, state)
+	local username = jid_split(data.from);
+	if data.action == "cancel" then
+		return { status = "canceled" };
+	end
+
+	if state == nil then
+		local prefs = get_prefs(username);
+		local values = {
+			default = {
+				{ value = "always", label = "Always" };
+				{ value = "never", label = "Never" };
+				{ value = "roster", label = "Roster" };
+			};
+			always = {};
+			never = {};
+		};
+
+		for jid, p in pairs(prefs) do
+			if jid then
+				t_insert(values[p and "always" or "never"], jid);
+
+			elseif p == true then -- Yes, this is ugly.  FIXME later.
+				values.default[1].default = true;
+			elseif p == false then
+				values.default[2].default = true;
+			elseif p == "roster" then
+				values.default[3].default = true;
+			end
+		end
+		return { status = "executing", actions  = { "complete" }, form = { layout = mam_prefs_form, values = values } }, true;
+	else
+		local fields = mam_prefs_form:data(data.form);
+
+		local default, always, never = fields.default, fields.always, fields.never;
+		local prefs = {};
+		if default then
+			prefs[false] = default_attrs[default];
+		end
+		if always then
+			for i=1,#always do
+				prefs[always[i]] = true;
+			end
+		end
+		if never then
+			for i=1,#never do
+				prefs[never[i]] = false;
+			end
+		end
+
+		set_prefs(username, prefs);
+
+		return { status = "completed" }
+	end
+end
+
+module:provides("adhoc", module:require"adhoc".new("Archive settings", "urn:xmpp:mam#configure", mam_prefs_handler, "local_user"));
--- a/mod_mam_muc/mod_mam_muc.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_mam_muc/mod_mam_muc.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -50,7 +50,7 @@
 	module:log("error", "Could not open archive storage");
 	return
 elseif not archive.find then
-	module:log("error", "mod_%s does not support archiving, switch to mod_storage_sql2", archive._provided_by);
+	module:log("error", "mod_%s does not support archiving", archive._provided_by);
 	return
 end
 
--- a/mod_manifesto/mod_manifesto.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_manifesto/mod_manifesto.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -57,14 +57,18 @@
 
 module:hook("resource-bind", function (event)
 	local session = event.session;
+	module:log("debug", "mod_%s sees that %s logged in", module.name, session.username);
 
 	local now = time();
 	local last_notify = notified[session.username] or 0;
 	if last_notify > ( now - 86400 * 7 ) then
+		module:log("debug", "Already notified %s", session.username);
 		return
 	end
 
+	module:log("debug", "Waiting 15 seconds");
 	timer.add_task(15, function ()
+		module:log("debug", "15 seconds later... session.type is %q", session.type);
 		if session.type ~= "c2s" then return end -- user quit already
 		local bad_contacts, bad_hosts = {}, {};
 		for contact_jid, item in pairs(session.roster or {}) do
@@ -96,6 +100,7 @@
 				end
 			end
 		end
+		module:log("debug", "%s has %d bad contacts", session.username, #bad_contacts);
 		if #bad_contacts > 0 then
 			local vars = {
 				HOST = host;
@@ -103,6 +108,7 @@
 				SERVICES = "    "..table.concat(bad_hosts, "\n    ");
 				CONTACTVIA = contact_method, CONTACT = contact;
 			};
+			module:log("debug", "Sending notification to %s", session.username);
 			session.send(st.message({ type = "headline", from = host }):tag("body"):text(message:gsub("$(%w+)", vars)));
 			notified[session.username] = now;
 		end
@@ -159,7 +165,7 @@
 		config_set(host, "s2s_require_encryption", true);
 
 		for _, session in pairs(s2s_sessions) do
-			if not session.secure then
+			if session.type == "s2sin" or session.type == "s2sout" and not session.secure then
 				(session.close or s2s_destroy_session)(session);
 			end
 		end
--- a/mod_message_logging/mod_message_logging.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_message_logging/mod_message_logging.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -52,7 +52,7 @@
 
 local function write_to_log(log_jid, jid, prefix, body)
 	if not body then return; end
-	local f = open_files[log_jid];
+	local f = open_files[jid_split(jid).."\0"..log_jid];
 	if not f then return; end
 	body = body:gsub("\n", "\n    "); -- Indent newlines
 	f:write(os.date("%H:%M:%S "), prefix or "", prefix and ": " or "", jid, ": ", body, "\n");
--- a/mod_motd_sequential/mod_motd_sequential.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_motd_sequential/mod_motd_sequential.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -7,33 +7,30 @@
 -- COPYING file in the source package for more information.
 --
 
-local host = module:get_host();
-local motd_jid = module:get_option("motd_jid") or host;
-local datamanager = require "util.datamanager";
-local ipairs = ipairs;
-local motd_sequential_messages = module:get_option("motd_sequential_messages") or {};
-local motd_messagesets = {};
-local max = 1;
-for i, message in ipairs(motd_sequential_messages) do
-    motd_messagesets[i] = message;
-    max = i;
-end
+local st = require "util.stanza";
+local jid_prep = require"util.jid".prep;
 
-local st = require "util.stanza";
+local motd_messages = module:get_option_array("motd_sequential_messages", {});
+local motd_jid = jid_prep(module:get_option_string("motd_jid", module.host));
+
+if #motd_messages == 0 then return; end
+if not motd_jid then module:log("error", "Invalid jid: %s", module:get_option_string("motd_jid", module.host)); return; end
+
+local seen = module:open_store("motd_sequential_seen");
 
-module:hook("resource-bind",
-    function (event)
-            local session = event.session;
-    local alreadyseen_list = datamanager.load(session.username, session.host, "motd_sequential_seen") or { max = 0 };
-    local alreadyseen = alreadyseen_list["max"] + 1;
-    local mod_stanza;
-    for i = alreadyseen, max do
-            motd_stanza =
-                    st.message({ to = session.username..'@'..session.host, from = motd_jid })
-                            :tag("body"):text(motd_messagesets[i]);
-            core_route_stanza(hosts[host], motd_stanza);
-            module:log("debug", "MOTD send to user %s@%s", session.username, session.host);
-    end
-    alreadyseen_list["max"] = max;
-    datamanager.store(session.username, session.host, "motd_sequential_seen", alreadyseen_list);
+module:hook("presence/bare", function (event)
+	local session, stanza = event.origin, event.stanza;
+	if session.username and not session.presence
+	and not stanza.attr.type and not stanza.attr.to then
+		local alreadyseen_list = seen:get(session.username) or { max = 0 };
+		local alreadyseen = alreadyseen_list.max + 1;
+		for i = alreadyseen, #motd_messages do
+			if motd_messages[i] then
+				session.send(st.message({ to = session.full_jid, from = motd_jid }, motd_messages[i]));
+				module:log("debug", "MOTD send to user %s@%s", session.username, session.host);
+			end
+		end
+		alreadyseen_list.max = max;
+		seen:set(session.username, alreadyseen_list);
+	end
 end);
--- a/mod_onions/mod_onions.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_onions/mod_onions.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,15 +1,17 @@
-local wrapclient = require "net.server".wrapclient;
+local addclient = require "net.server".addclient;
 local s2s_new_outgoing = require "core.s2smanager".new_outgoing;
 local initialize_filters = require "util.filters".initialize;
 local st = require "util.stanza";
+local log = module._log;
+local core_process_stanza = prosody.core_process_stanza;
+local hosts = prosody.hosts;
 
 local portmanager = require "core.portmanager";
 
 local softreq = require "util.dependencies".softreq;
 
-local bit;
-pcall(function() bit = require"bit"; end);
-bit = bit or softreq"bit32"
+local socket = require"socket";
+local bit = softreq"bit" or softreq"bit32"
 if not bit then module:log("error", "No bit module found. Either LuaJIT 2, lua-bitop or Lua 5.2 is required"); end
 
 local band = bit.band;
@@ -59,14 +61,11 @@
 		end
 
 		-- this means the server tells us to connect on an IPv4 address
-		local ip1 = byte(data, 5);
-		local ip2 = byte(data, 6);
-		local ip3 = byte(data, 7);
-		local ip4 = byte(data, 8);
+		local ip = ("%d.%d.%d.%d"):format(byte(data, 5, 8));
 		local port = band(byte(data, 9), lshift(byte(data, 10), 8));
-		module:log("debug", "Should connect to: "..ip1.."."..ip2.."."..ip3.."."..ip4..":"..port);
+		module:log("debug", "Should connect to: %s:%d", ip, port);
 
-		if not (ip1 == 0 and ip2 == 0 and ip3 == 0 and ip4 == 0 and port == 0) then
+		if not (ip == "0.0.0.0" and port == 0) then
 			module:log("debug", "The SOCKS5 proxy tells us to connect to a different IP, don't know how. :(");
 			session:close(false);
 			return;
@@ -94,7 +93,7 @@
 			if t then
 				t = filter("bytes/out", tostring(t));
 				if t then
-					return conn:write(tostring(t));
+					return w(conn, t);
 				end
 			end
 		end
@@ -138,7 +137,7 @@
 	module:log("debug", "Sending connect message.");
 
 	-- version 5, connect, (reserved), type: domainname, (length, hostname), port
-	conn:write(c(5) .. c(1) .. c(0) .. c(3) .. c(#session.socks5_to) .. session.socks5_to);
+	conn:write("\5\1\0\3" .. c(#session.socks5_to) .. session.socks5_to);
 	conn:write(c(rshift(session.socks5_port, 8)) .. c(band(session.socks5_port, 0xff)));
 
 	session.socks5_handler = socks5_connect_sent;
@@ -148,7 +147,7 @@
 	module:log("debug", "Connected to SOCKS5 proxy, sending SOCKS5 handshake.");
 
 	-- Socks version 5, 1 method, no auth
-	conn:write(c(5) .. c(1) .. c(0));
+	conn:write("\5\1\0");
 
 	sessions[conn].socks5_handler = socks5_handshake_sent;
 end
@@ -176,19 +175,13 @@
 
 local function connect_socks5(host_session, connect_host, connect_port)
 
-	local conn, handler = socket.tcp();
-
 	module:log("debug", "Connecting to " .. connect_host .. ":" .. connect_port);
 
 	-- this is not necessarily the same as .to_host (it can be that this is a SRV record)
 	host_session.socks5_to = connect_host;
 	host_session.socks5_port = connect_port;
 
-	conn:settimeout(0);
-
-	local success, err = conn:connect(proxy_ip, proxy_port);
-
-	conn = wrapclient(conn, connect_host, connect_port, socks5listener, "*a");
+	local conn, err = addclient( proxy_ip, proxy_port, socks5listener, '*a', nil );
 
 	socks5listener.register_outgoing(conn, host_session);
 
@@ -229,7 +222,7 @@
 
 	if not event.to_host:find(".onion(.?)$") then
 		if forbid_else then
-	                module:log("debug", event.to_host .. " is not an onion. Blocking it.");
+			module:log("debug", event.to_host .. " is not an onion. Blocking it.");
 			return false;
 		elseif not torify_all then
 			return;
@@ -256,4 +249,4 @@
 
 module:log("debug", "Onions ready and loaded");
 
-hosts[module.host].events.add_handler("route/remote", route_to_onion, 200);
+module:hook("route/remote", route_to_onion, 200);
--- a/mod_profile/mod_profile.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_profile/mod_profile.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,7 +1,8 @@
 -- mod_profile
 
 local st = require"util.stanza";
-local jid_split, jid_bare = import("util.jid", "split", "bare");
+local jid_split = require"util.jid".split;
+local jid_bare = require"util.jid".bare;
 local is_admin = require"core.usermanager".is_admin;
 local vcard = require"util.vcard";
 local base64 = require"util.encodings".base64;
@@ -86,7 +87,7 @@
 	local username = origin.username;
 	local to = stanza.attr.to;
 	if to then username = jid_split(to); end
-	local data = storage:get(username);
+	local data, err = storage:get(username);
 	if not data then
 		data = legacy_storage:get(username);
 		data = data and st.deserialize(data);
--- a/mod_rawdebug/mod_rawdebug.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_rawdebug/mod_rawdebug.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -19,14 +19,15 @@
 	end
 end
 
-function rawdebug:enable(sessionid)
+function rawdebug:enable(sessionid, typ)
 	local session = full_sessions[sessionid];
+	typ = typ or "stanzas";
 	if not session then
 		return nil, "No such session";
 	end
 	local f = {
-		["stanzas/in"]  = new_logger(session.log or log, "RECV");
-		["stanzas/out"] = new_logger(session.log or log, "SEND");
+		[typ .. "/in"]  = new_logger(session.log or log, "RECV");
+		[typ .. "/out"] = new_logger(session.log or log, "SEND");
 	};
 	for type, callback in pairs(f) do
 		filters.add_filter(session, type, callback)
--- a/mod_register_redirect/mod_register_redirect.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_register_redirect/mod_register_redirect.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,88 +1,13 @@
--- (C) 2010-2011 Marco Cirillo (LW.Org)
--- (C) 2011 Kim Alvefur
---
--- Registration Redirect module for Prosody
---
--- Redirects IP addresses not in the whitelist to a web page or another method to complete the registration.
-
-local st = require "util.stanza"
-local cman = configmanager
+local st = require"util.stanza";
 
-local ip_wl = module:get_option_set("registration_whitelist", { "127.0.0.1" })
-local url = module:get_option_string("registration_url", nil)
-local inst_text = module:get_option_string("registration_text", nil)
-local oob = module:get_option_boolean("registration_oob", true)
-local admins_g = cman.get("*", "core", "admins")
-local admins_l = cman.get(module:get_host(), "core", "admins")
-local no_wl = module:get_option_boolean("no_registration_whitelist", false)
-
-if type(admins_g) ~= "table" then admins_g = nil end
-if type(admins_l) ~= "table" then admins_l = nil end
-
-function reg_redirect(event)
-	local stanza, origin = event.stanza, event.origin
-
-	if not no_wl and ip_wl:contains(origin.ip) then return; end
+local oob_url = module:get_option_string(module.name .. "_url", "http://www.example.com");
+local instructions = module:get_option_string(module.name .. "_instructions", "To register, please visit "..oob_url);
 
-	-- perform checks to set default responses and sanity checks.
-	if not inst_text then
-		if url and oob then
-			if url:match("^%w+[:].*$") then
-				if url:match("^(%w+)[:].*$") == "http" or url:match("^(%w+)[:].*$") == "https" then
-					inst_text = "Please visit "..url.." to register an account on this server."
-				elseif url:match("^(%w+)[:].*$") == "mailto" then
-					inst_text = "Please send an e-mail at "..url:match("^%w+[:](.*)$").." to register an account on this server."
-				elseif url:match("^(%w+)[:].*$") == "xmpp" then
-					inst_text = "Please contact "..module:get_host().."'s server administrator via xmpp to register an account on this server at: "..url:match("^%w+[:](.*)$")
-				else
-					module:log("error", "This module supports only http/https, mailto or xmpp as URL formats.")
-					module:log("error", "If you want to use personalized instructions without an Out-Of-Band method,")
-					module:log("error", "specify: register_oob = false; -- in your configuration along your banner string (register_text).")
-					return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request.
-				end
-			else
-				module:log("error", "Please check your configuration, the URL you specified is invalid")
-				return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request.
-			end
-		else
-			if admins_l then
-				local ajid; for _,v in ipairs(admins_l) do ajid = v ; break end
-				inst_text = "Please contact "..module:get_host().."'s server administrator via xmpp to register an account on this server at: "..ajid
-			else
-				if admins_g then
-					local ajid; for _,v in ipairs(admins_g) do ajid = v ; break end
-					inst_text = "Please contact "..module:get_host().."'s server administrator via xmpp to register an account on this server at: "..ajid
-				else
-					module:log("error", "Please be sure to, _at the very least_, configure one server administrator either global or hostwise...")
-					module:log("error", "if you want to use this module, or read it's configuration wiki at: http://code.google.com/p/prosody-modules/wiki/mod_register_redirect")
-					return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request.
-				end
-			end
-		end
-	elseif inst_text and url and oob then
-		if not url:match("^%w+[:].*$") then
-			module:log("error", "Please check your configuration, the URL specified is not valid.")
-			return origin.send(st.error_reply(stanza, "wait", "internal-server-error")) -- bouncing request.
-		end
-	end
+module:hook("stanza/iq/jabber:iq:register:query", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	origin.send(st.reply(stanza):query("jabber:iq:register")
+		:tag("instructions"):text(instructions):up()
+		:tag("x", { xmlns="jabber:x:oob" }):tag("url"):text(oob_url));
+	return true;
+end, 10);
 
-	-- Prepare replies.
-	local reply = st.reply(event.stanza)
-	if oob then
-		reply:query("jabber:iq:register")
-			:tag("instructions"):text(inst_text):up()
-			:tag("x", {xmlns = "jabber:x:oob"})
-				:tag("url"):text(url);
-	else
-		reply:query("jabber:iq:register")
-			:tag("instructions"):text(inst_text):up()
-	end
-
-	if stanza.attr.type == "get" then
-		return origin.send(reply)
-	else
-		return origin.send(st.error_reply(stanza, "cancel", "not-authorized"))
-	end
-end
-
-module:hook("stanza/iq/jabber:iq:register:query", reg_redirect, 10)
--- a/mod_register_web/mod_register_web.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_register_web/mod_register_web.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -101,8 +101,9 @@
 end
 
 function generate_page(event, display_options)
-	local request = event.request;
+	local request, response = event.request, event.response;
 
+	response.headers.content_type = "text/html; charset=utf-8";
 	return render(register_tpl, {
 		path = request.path; hostname = module.host;
 		notice = display_options and display_options.register_error or "";
@@ -154,6 +155,7 @@
 
 function generate_register_response(event, form, ok, err)
 	local message;
+	event.response.headers.content_type = "text/html; charset=utf-8";
 	if ok then
 		return generate_success(event, form);
 	else
--- a/mod_s2s_auth_dane/mod_s2s_auth_dane.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_s2s_auth_dane/mod_s2s_auth_dane.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -89,7 +89,12 @@
 							t_insert(dane, record);
 						end
 					end
-					if n == 0 and cb then return cb(a,b,c,e); end
+					if n == 0 and cb then
+						if #dane == 0 then
+							host_session.dane = false;
+						end
+						return cb(a,b,c,e);
+					end
 				end, ("_%d._tcp.%s."):format(record.srv.port, record.srv.target), "TLSA");
 			end
 		end, "_xmpp-server._tcp."..name..".", "SRV");
@@ -116,12 +121,12 @@
 		local host_session = event.origin;
 		if host_session.type == "s2sout" or host_session.type == "s2sin" or host_session.dane ~= nil then return end -- Already authenticated
 		local function resume()
-			host_session.log("debug", "DANE lookup completed, resuming connection");
-			host_session.conn:resume()
+			host_session.log("debug", "DANE lookup completed");
+			host_session.unlock("dane");
 		end
 		if dane_lookup(host_session, resume) then
-			host_session.log("debug", "Pausing connection until DANE lookup is completed");
-			host_session.conn:pause()
+			host_session.log("debug", "Locking session until DANE lookup is completed");
+			host_session.lock("dane");
 		end
 	end
 
@@ -134,7 +139,7 @@
 
 	module:hook("s2s-authenticated", function(event)
 		local session = event.session;
-		if session.dane and not session.secure then
+		if session.dane and next(session.dane) ~= nil and not session.secure then
 			-- TLSA record but no TLS, not ok.
 			-- TODO Optional?
 			-- Bogus replies should trigger this path
--- a/mod_s2s_auth_fingerprint/mod_s2s_auth_fingerprint.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_s2s_auth_fingerprint/mod_s2s_auth_fingerprint.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -8,11 +8,11 @@
 local fingerprints = {};
 
 local function hashprep(h)
-	return tostring(h):gsub(":",""):lower();
+	return tostring(h):gsub("%X+",""):lower();
 end
 
 local function hashfmt(h)
-	return h:gsub("..",":%0"):sub(2):upper();
+	return h:gsub("..","%0:", #h/2-1):upper();
 end
 
 for host, set in pairs(module:get_option("s2s_trusted_fingerprints", {})) do
@@ -29,19 +29,21 @@
 
 module:hook("s2s-check-certificate", function(event)
 	local session, host, cert = event.session, event.host, event.cert;
+	local log = session.log or module._log;
 
 	local host_fingerprints = fingerprints[host];
 	if host_fingerprints then
 		local digest = cert and cert:digest(digest_algo);
 		if host_fingerprints[digest] then
-			module:log("info", "'%s' matched %s fingerprint %s", host, digest_algo:upper(), hashfmt(digest));
+			log("info", "'%s' matched %s fingerprint %s", host, digest_algo:upper(), hashfmt(digest));
 			session.cert_chain_status = "valid";
 			session.cert_identity_status = "valid";
 			return true;
 		else
-			module:log("warn", "'%s' has unknown %s fingerprint %s", host, digest_algo:upper(), hashfmt(digest));
+			log("warn", "'%s' has unknown %s fingerprint %s", host, digest_algo:upper(), hashfmt(digest));
 			session.cert_chain_status = "invalid";
 			session.cert_identity_status = "invalid";
+			-- return false;
 		end
 	end
-end);
+end, 0);
--- a/mod_s2s_auth_monkeysphere/mod_s2s_auth_monkeysphere.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_s2s_auth_monkeysphere/mod_s2s_auth_monkeysphere.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -19,7 +19,7 @@
 			type = "peer";
 		};
 		context = "https";
-		-- context = "xmpp"; -- Monkeysphere needs to be extended to understand this
+		-- context = "xmpp-server"; -- Monkeysphere needs to be extended to understand this
 		pkc = {
 			type = "x509pem";
 			data = cert:pem();
--- a/mod_s2s_keysize_policy/mod_s2s_keysize_policy.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_s2s_keysize_policy/mod_s2s_keysize_policy.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -26,9 +26,9 @@
 	if cert and cert.pubkey then
 		local _, key_type, key_size = cert:pubkey();
 		if key_size < ( weak_key_size[key_type] or 0 ) then
-			local issued = parse_x509_datetime(cert:notbefore());
-			if issued > weak_key_cutoff then
-				session.log("error", "%s has a %s-bit %s key issued after 31 December 2013, invalidating trust!", host, key_size, key_type);
+			local expires = parse_x509_datetime(cert:notafter());
+			if expires > weak_key_cutoff then
+				session.log("error", "%s has a %s-bit %s key valid after 31 December 2013, invalidating trust!", host, key_size, key_type);
 				session.cert_chain_status = "invalid";
 				session.cert_identity_status = "invalid";
 			else
--- a/mod_s2s_log_certs/mod_s2s_log_certs.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_s2s_log_certs/mod_s2s_log_certs.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -24,6 +24,7 @@
 		digest_algo:upper(),
 		digest:upper():gsub("..",":%0"):sub(2));
 
+	-- TODO Include CA info
 	if do_store then
 		local seen_certs = dm_load(remote_host, local_host, "s2s_certs") or {};
 
--- a/mod_smacks/mod_smacks.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_smacks/mod_smacks.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -24,7 +24,7 @@
 local sessionmanager = require"core.sessionmanager";
 
 local c2s_sessions = module:shared("/*/c2s/sessions");
-local session_registry = {};
+local session_registry = module:shared("sessions");
 
 local function can_do_smacks(session, advertise_only)
 	if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
@@ -58,7 +58,7 @@
 		end);
 
 local function outgoing_stanza_filter(stanza, session)
-	local is_stanza = stanza.attr and not stanza.attr.xmlns;
+	local is_stanza = stanza.attr and not stanza.attr.xmlns and not stanza.name:find":";
 	if is_stanza and not stanza._cached then -- Stanza in default stream namespace
 		local queue = session.outgoing_stanza_queue;
 		local cached_stanza = st.clone(stanza);
@@ -151,22 +151,20 @@
 module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
 
 module:hook_stanza("http://etherx.jabber.org/streams", "features",
-		function (session, stanza)
-			module:add_timer(0, function ()
-				if can_do_smacks(session) then
-					if stanza:get_child("sm", xmlns_sm3) then
-						session.sends2s(st.stanza("enable", sm3_attr));
-						session.smacks = xmlns_sm3;
-					elseif stanza:get_child("sm", xmlns_sm2) then
-						session.sends2s(st.stanza("enable", sm2_attr));
-						session.smacks = xmlns_sm2;
-					else
-						return;
-					end
-					wrap_session_out(session, false);
-				end
-			end);
-		end);
+function (session, stanza)
+	if can_do_smacks(session) then
+		if stanza:get_child("sm", xmlns_sm3) then
+			session.sends2s(st.stanza("enable", sm3_attr));
+			session.smacks = xmlns_sm3;
+		elseif stanza:get_child("sm", xmlns_sm2) then
+			session.sends2s(st.stanza("enable", sm2_attr));
+			session.smacks = xmlns_sm2;
+		else
+			return;
+		end
+		wrap_session_out(session, false);
+	end
+end);
 
 function handle_enabled(session, stanza, xmlns_sm)
 	module:log("debug", "Enabling stream management");
@@ -233,12 +231,10 @@
 	local error_attr = { type = "cancel" };
 	if #queue > 0 then
 		session.outgoing_stanza_queue = {};
+		local reply;
 		for i=1,#queue do
-			local reply = st.reply(queue[i]);
-			if reply.attr.to ~= session.full_jid then
-				reply.attr.type = "error";
-				reply:tag("error", error_attr)
-					:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
+			if queue[i].attr.from ~= session.full_jid then
+				queue[i], reply = nil, st.error_reply(queue[i], "recipient-unavailable");
 				core_process_stanza(session, reply);
 			end
 		end
@@ -266,7 +262,7 @@
 				-- (for example, the client may have bound a new resource and
 				-- started a new smacks session, or not be using smacks)
 				local curr_session = full_sessions[session.full_jid];
-				if false and session.destroyed then
+				if session.destroyed then
 					session.log("debug", "The session has already been destroyed");
 				elseif curr_session and curr_session.resumption_token == resumption_token
 				-- Check the hibernate time still matches what we think it is,
@@ -351,3 +347,26 @@
 end
 module:hook_stanza(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
 module:hook_stanza(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
+
+local function handle_read_timeout(event)
+	local session = event.session;
+	local xmlns_sm = session.smacks;
+	if xmlns_sm then
+		session.awaiting_ack = true;
+		return (session.sends2s or session.send)(st.stanza("r", { xmlns = xmlns_sm }));
+	end
+end
+
+module:hook("s2s-read-timeout", handle_read_timeout, 10);
+module:hook("c2s-read-timeout", handle_read_timeout, 10);
+
+local function handle_s2s_destroyed(event)
+	local session = event.session;
+	local queue = session.outgoing_stanza_queue;
+	if queue and #queue > 0 then
+		session.log("warn", "Destroying session with %d unacked stanzas", #queue);
+		handle_unacked_stanzas(session);
+	end
+end;
+module:hook("s2sout-destroyed", handle_s2s_destroyed);
+module:hook("s2sin-destroyed", handle_s2s_destroyed);
--- a/mod_srvinjection/mod_srvinjection.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_srvinjection/mod_srvinjection.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,3 +1,4 @@
+local s = require"util.serialization".new"oneline".serialize;
 
 module:set_global();
 
@@ -25,8 +26,12 @@
 
 local original_lookup = adns.lookup;
 function adns.lookup(handler, qname, qtype, qclass)
+	module:log("debug", "adns.lookup(%s, %s, %s)", s(qname), s(qtype), s(qclass));
 	if qtype == "SRV" then
 		local host = qname:match("^_xmpp%-server%._tcp%.(.*)%.$");
+		module:log("debug", "qname:match(...) → %s", s(host));
+		local mapping = map[host] or map["*"];
+		module:log("debug", "map[%s] → %s", s(host), s(mapping));
 		local mapping = map[host] or map["*"];
 		if mapping then
 			handler(mapping);
--- a/mod_storage_gdbm/mod_storage_gdbm.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_storage_gdbm/mod_storage_gdbm.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -9,7 +9,9 @@
 local gdbm = require"gdbm";
 local path = require"util.paths";
 local lfs = require"lfs";
-local serialize, deserialize = import("util.serialization", "serialize", "deserialize");
+local serialization = require"util.serialization";
+local serialize = serialization.serialize;
+local deserialize = serialization.deserialize;
 
 local base_path = path.resolve_relative_path(prosody.paths.data, module.host);
 lfs.mkdir(base_path);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_storage_internal_ng/json.lib.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -0,0 +1,2 @@
+local json = require"util.json";
+return json;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_storage_internal_ng/lua.lib.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -0,0 +1,13 @@
+
+
+if serialization.new then
+	local settings = module:get_option(module.name .. "_lua", "compact");
+	serialization = serialization.new(settings);
+	serialize = serialization.serialize;
+end
+
+return {
+	encode = serialize;
+	decode = deserialize;
+};
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_storage_internal_ng/mod_storage_internal_ng.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -0,0 +1,713 @@
+-- Copyright (C) 2014-2015 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+
+local tonumber = tonumber;
+local tostring = tostring;
+local type = type;
+local io_open = io.open;
+local os_remove = os.remove;
+local os_rename = os.rename;
+
+local noop = function () end
+local id = function (x) return x end
+
+local lfs = require"lfs";
+local st = require"util.stanza";
+local xml = require"util.xml";
+local sha256 = require"util.hashes".sha256;
+local datetime = require"util.datetime";
+local path_join = require"util.paths".join;
+local http = require"util.http";
+
+local encode, decode = import("util.serialization", "serialize", "deserialize");
+local decode_file = encoder.decode_file;
+
+local log = module._log;
+local base_path = prosody.paths.data;
+
+local suffix = ".dat";
+
+-- No path escaping, assume no forbidden chars in names
+local function getpath(...)
+	return path_join(base_path, module.host, ...);
+end
+
+local did_mkdir_already = {};
+local function mkdir(path)
+	if not did_mkdir_already[path] then
+		did_mkdir_already[path] = true;
+		return lfs.mkdir(path);
+	end
+end
+
+function module.load()
+	mkdir(getpath()); -- Create base dir for host
+end
+
+-- Cleanup
+local function cleanup(scratch, f, msg)
+	if f then f:close(); end
+	os_remove(scratch);
+	return nil, msg;
+end
+
+local function atomic_store(filename, data)
+	local scratch = filename.."~";
+	local f, ok, msg;
+	f, msg = io_open(scratch, "wb");
+	if not f then return cleanup(scratch, f, msg) end
+
+	ok, msg = f:write(data);
+	if not ok then return cleanup(scratch, f, msg) end
+
+	ok, msg = f:close();
+	if not ok then return cleanup(scratch, f, msg) end
+
+	return os_rename(scratch, filename);
+end
+
+local function read_fh(fh, max_length)
+	return decode(fh:read(max_length or "*a"));
+end
+
+local function read_file(path, max_length)
+	local f, err = io_open(path, "rb");
+	if not f then
+		return f, err;
+	end
+
+	local data = read_fh(fh, max_length);
+	f:close();
+	return data;
+end
+
+local function log_error(path, store, err, user)
+	local mode = lfs.attributes(path, "mode");
+	if not mode then
+		log("debug", "Assuming empty %s storage ('%s') for user: %s", store, err, user or "nil");
+		return nil;
+	else -- file exists, but can't be read
+		-- TODO more detailed error checking and logging?
+		log("error", "Failed to load %s storage ('%s') for user: %s", store, err, user or "nil");
+		return nil, "Error reading storage";
+	end
+end
+
+local function h(s)
+	return sha256(s, true):sub(1, 2);
+end
+local function new_keyval_store(store)
+	local driver = { store = store, typ = "keyval" };
+
+	function driver:get(user)
+		user = user or "@";
+		local path = getpath(user, store) .. suffix;
+		local data, err = read_file(path);
+		if not data then return log_error(path, store, err, user or "<nil>") end
+		return data;
+	end
+	function driver:set(user, data)
+		user = user or "@";
+		mkdir(getpath(user));
+		local path = getpath(user, store) .. suffix;
+		if data == nil then
+			if lfs.attributes(path, "mode") == "file" then
+				return os_remove(path);
+			end
+			return true;
+		end
+		data = encode(data);
+		return atomic_store(path, data);
+	end
+	function driver:users()
+		local next, state = lfs.dir(getpath());
+		return function (state)
+			for node in next, state do
+				if lfs.attributes(getpath(node, store) .. suffix, "mode") == "file" then
+					return node;
+				end
+			end
+		end, state;
+	end
+	return driver;
+end
+
+local function new_map_store(store, as_keyval)
+	local driver = { store = store, typ = "map" };
+
+	-- FIXME Keys may be encoded to values longer than the file system supports
+
+	function driver:get(user, key)
+		assert(key ~= nil, "map key can't be nil");
+		user = user or "@";
+		key = http.urlencode(encode(key));
+		local path = getpath(user, store, key) .. suffix;
+		local data, err = read_file(path);
+		if not data then return log_error(path, store, err, user or "<nil>") end
+		return data;
+	end
+
+	function driver:set(user, key, data)
+		assert(key ~= nil, "map key can't be nil");
+		user = user or "@";
+		key = http.urlencode(encode(key));
+		mkdir(getpath(user));
+		mkdir(getpath(user, store));
+		local path = getpath(user, store, key) .. suffix;
+		if data == nil then
+			return os_remove(path);
+		end
+		data = encode(data);
+		return atomic_store(path, data);
+	end
+
+	function driver:keys(user)
+		user = user or "@";
+		local ok, iter, state, var = pcall(lfs.dir, getpath(user, store));
+		if not ok then
+			log("warn", iter);
+			return noop;
+		end
+		return function (state, var)
+			local item;
+			repeat
+				item = iter(state, var);
+				if item == nil then return end
+			until lfs.attributes(getpath(user,store, item), "mode") == "file" and item:sub(-#suffix) == suffix;
+			return decode(http.urldecode(item:sub(1, -#suffix-1)));
+		end, state, var;
+	end
+
+	function driver:pairs(user)
+		local iter, state, var = self:keys(user);
+		return function (state, var)
+			local key = iter(state, var);
+			if key == nil then return end
+			return key, self:get(user, key);
+		end, state, var;
+	end
+
+	function driver:empty(user)
+		for key in self:keys(user) do
+			self:set(user, key, nil);
+		end
+		return true;
+	end
+
+	function driver:collect(user)
+		local data = {};
+		for k,v in self:pairs(user) do
+			data[k] = v;
+		end
+		return data;
+	end
+
+	if as_keyval then
+		local keyval_driver = new_keyval_store(store);
+
+		function keyval_driver:get(user)
+			return driver:collect(user);
+		end
+
+		function keyval_driver:set(user, data)
+			driver:empty(user);
+			if data ~= nil then
+				for k, v in pairs(data) do
+					driver:set(user, k, v);
+				end
+			end
+		end
+
+		return keyval_driver;
+	end
+
+	return driver;
+end
+
+-- date utilities
+local function tomorrow(date)
+	return datetime.date(datetime.parse(date .. "T12:00:00Z") + 86400);
+end
+
+local function yesterday(date)
+	return datetime.date(datetime.parse(date .. "T12:00:00Z") - 86400);
+end
+
+local function new_archive_store(store)
+	local driver = { store = store, typ = "archive" };
+	local fallocate = require"util.pposix".fallocate or function (f, o, l)
+		local ok, err = f:write(string.rep(" ", l));
+		if not ok then return ok, err end
+		return f:seek("set", o);
+	end
+
+	local index_storage = new_map_store(store);
+	local _get = index_storage.get;
+	function index_storage:get(user, key)
+		local ok, err = _get(self, user, key);
+		if not ok then
+			log("debug", tostring(err));
+		end
+		return ok, err;
+	end
+
+	local datelists = setmetatable({}, { __mode = 'v'; });
+
+	local function get_datelist(user, with)
+		local key = with and (user..'\0'..with) or user;
+
+		local list = datelists[key];
+		if list then return list end
+
+		if with then
+			local datemap = index_storage:get(user, with);
+			if datemap then
+				list = {};
+				local i = 1;
+				for date in pairs(datemap) do
+					list[i], i = date, i+1;
+				end
+			end
+		end
+
+		if not list then
+			list = {};
+			local ok, iter, state, var = pcall(lfs.dir, getpath(user, store));
+			if not ok then return list; end
+
+			local i = 1;
+			local filename, mode, date;
+			for item in iter, state, var do
+				filename = getpath(user, store, item);
+				mode = lfs.attributes(filename, "mode");
+				if mode == "file" and item:match("^%d%d%d%d%-%d%d%-%d%d") and item:sub(-#suffix) == suffix then
+					list[i], i = item:sub(1,10), i+1;
+				end
+			end
+		end
+
+		table.sort(list);
+		for i = 1, #list do
+			list[ list[i] ] = i;
+		end
+		datelists[key] = list;
+		return list;
+	end
+
+	function driver:append(user, key, when, with, value)
+		user = user or "@";
+		local ok;
+		local date = datetime.date(when);
+		mkdir(getpath(user));
+		mkdir(getpath(user, store));
+		local filename = getpath(user, store, date) .. suffix;
+		local f, msg = io_open(filename, "r+b");
+		if not f then
+			f, msg = io_open(filename, "wb");
+		end
+		if not f then
+			return f, msg;
+		end
+		local mt = getmetatable(value);
+		local typ = type(value);
+		--[[
+		if mt.__freeze then
+			value, typ = mt.__freeze(value);
+		else--]]
+		if mt == st.stanza_mt then
+			value, typ = st.preserialize(value), "stanza";
+			-- if false then
+				-- value, typ = tostring(value), "xml";
+			-- else
+			-- end
+		end
+		local pos = f:seek("end");
+		key = ("%sp%x"):format(date, pos);
+		key = key .. "r".. sha256(user..key, true):sub(1, 8);
+		local metadata = encode({ key, datetime.datetime(when), with, typ });
+		local data = encode(value);
+		local meta_len, data_len = #metadata, #data;
+		local meta_ls, data_ls = ("%d"):format(meta_len), ("%d"):format(data_len);
+		local total_ls = ("%d"):format(meta_len + #meta_ls + data_len + #data_ls + 4); -- two newlines?
+		-- ok, msg = fallocate(f, pos, data_len + #len_str * 2 + 3);
+		-- if not ok then return ok, msg; end
+		ok, msg = f:write(meta_ls, "\n", metadata, "\n", data_ls, "\n", data, "\n", total_ls, "\n");
+		if not ok then return ok, msg; end
+		ok, msg = f:close();
+		if not ok then return ok, msg; end
+		datelists[user] = nil;
+		if with then datelists[user .. "\0" .. with] = nil end
+		local index = index_storage:get(user, with) or {};
+		if not index[date] then
+			index[date] = true;
+			index_storage:set(user, with, index);
+		end
+		return key;
+	end
+
+	local unfreeze = {
+		xml = require"util.xml".parse;
+		stanza = st.deserialize;
+	}
+
+	local function valid_key(user, key)
+		if not key then return nil, "no-key"; end
+		if sha256(user..key:match("^(.-)r"), 1):sub(1, 8) ~= key:match("r(.*)$") then
+			log("warn", "invalid-key-signature");
+			-- return nil, "invalid-key-signature";
+		end
+		local when = datetime.parse(key:sub(1, 10).."T00:00:00Z");
+		if not when then return nil, "invalid-key-date"; end
+		local pos = key:match("^p(%x+)r", 11);
+		pos = tonumber(pos, 16);
+		if not pos then return nil, "invalid-key-offset"; end
+		return when, pos;
+	end
+
+	function driver:get(user, key)
+		local date, seek = valid_key(user, key);
+		if not date then return nil, seek; end
+		date = datetime.date(date);
+		local filename = getpath(user, store, date) .. suffix;
+		local fh, err = io_open(filename, "rb");
+		if not fh then
+			return nil, err;
+		end
+		fh:seek("set", seek);
+		local len = fh:read("*l");
+		local meta = fh:read(tonumber(len));
+		meta = decode(meta);
+		if not meta or meta[1] ~= key then
+			fh:close();
+			return nil, "not found";
+		end
+		len = fh:read("*l");
+		local data = fh:read(tonumber(len));
+		fh:close();
+		data = decode(data);
+		local when = meta[2];
+		if type(when) == "string" then
+			when = datetime.parse(when);
+		end
+		return (unfreeze[ meta[4] ] or id)(data), when, meta[3];
+	end
+
+	local NULL = {};
+
+	-- The ones in math doesn't support strings
+	local function max(a, b) return a > b and a or b; end
+	local function min(a, b) return a < b and a or b; end
+
+	function driver:_update_index(user, query)
+		index_storage:empty();
+		local with_index = {};
+		local iter, err = self:find(user, query);
+		if not iter then
+			log("error", tostring(err));
+			return nil, err;
+		end
+		local date = datetime.date;
+		for key, message, when, with in iter do
+			log("debug", "Item %s with %s", key, with);
+			if with then
+				local with_dates = with_index[with];
+				if not with_dates then
+					with_dates = {};
+					with_index[with] = with_dates;
+				end
+				when = date(when);
+				with_dates[when] = true;
+			end
+		end
+		for key, value in pairs(with_index) do
+			log("debug", "Write index %s", key);
+			index_storage:set(user, key, value);
+		end
+	end
+
+	local function find_sol(fh) -- Find Start of Line
+		local pos;
+		repeat
+			pos = fh:seek("cur", -2);
+		until not pos or pos == 0 or fh:read(1) == "\n";
+		return pos;
+	end
+
+	function driver:find(user, query)
+		query = query or NULL;
+		user = user or "@";
+		local datelist = get_datelist(user, query.with);
+		if not datelist[1] then return noop, 0 end
+
+		local start = query.start or 0;
+		local ending = query["end"] or 0x7fffffff;
+		local start_date = max(datelist[1], datetime.date(start));
+		local end_date = min(datelist[#datelist], datetime.date(ending));
+
+		local seek_once = query.reverse and query.before or query.after;
+		local date = query.reverse and end_date or start_date;
+		if seek_once then
+			local t;
+			t, seek_once = valid_key(user, seek_once);
+			if not t then return nil, seek_once; end
+			date = datetime.date(t);
+		end
+		local limit, results = query.limit, 0;
+		local log = function () end
+
+		if query.reverse then
+			-- log("debug", "find first date");
+			if seek_once == 0 then
+				date = yesterday(date);
+				seek_once = nil;
+			end
+			while not datelist[date] and date >= start_date do
+				date = yesterday(date);
+			end
+			-- log("debug", "datelist[%q] = %s", date, tostring(datelist[date]));
+			-- log("debug", "%s <= %s → %s", tostring(date), tostring(start_date), tostring(date <= start_date));
+			-- log("warn", "reverse");
+			return coroutine.wrap(function ()
+				local filename, fh, err, len, pos, meta, data, when;
+				-- log("debug", "enter outer loop");
+				-- log("debug", "date = %s", tostring(date));
+				while date and datelist[date] and date >= start_date do
+					-- log("debug", "iterate outer loop");
+
+					filename = getpath(user, store, date) .. suffix;
+					fh, err = io_open(filename, "rb");
+					if not fh then
+						log("warn", err);
+					else
+						if seek_once then
+							pos = fh:seek("set", seek_once);
+							seek_once = nil;
+						else
+							fh:seek("end");
+						end
+						pos = find_sol(fh);
+						if pos and pos > 1 then
+							len = fh:read("*l");
+							pos = fh:seek("cur", -tonumber(len)-1);
+							pos = find_sol(fh);
+						end
+						if pos < 2 then pos = fh:seek("set", 0); end
+						len = fh:read("*l");
+						log("debug", "enter inner loop");
+						while len do
+							meta = fh:read(tonumber(len));
+							fh:seek("cur", 1);
+							meta = meta and decode(meta);
+							if not meta then break; end
+							len = fh:read("*l");
+							-- log("debug", "{ %q, %d, %q, %q, <%s> }", data[1], data[2], data[3], data[4], type(data[5]));
+							when = meta[2];
+							if type(when) == "string" then
+								when = datetime.parse(when);
+							end
+							if when >= start and when <= ending and (not query.with or meta[3] == query.with) then
+								data = fh:read(tonumber(len));
+								-- log("debug", "data is %s:%s", type(data), tostring(data));
+								data = decode(data);
+								coroutine.yield(meta[1], (unfreeze[ meta[4] ] or id)(data), when, meta[3]);
+								results = results + 1;
+								if limit and results >= limit then
+									-- log("debug", "results => limit");
+									return;
+								end 
+							end
+							if not pos or pos == 0 then
+								-- We just read the first entry, proceed to the last in the previous file
+								break
+							end
+							pos = fh:seek("set", pos); -- Skip to top of the entry we just parsed
+							pos = find_sol(fh); -- Find previous newline
+							len = fh:read("*l"); -- Read the trailing length (meta + data)
+							pos = len and fh:seek("cur", -tonumber(len) - #len);
+							pos = find_sol(fh);
+							if not pos or pos == 1 then fh:seek("set", 0); pos = nil; end
+							len = fh:read("*l");
+						end
+					end
+					fh = fh:close();
+					-- log("debug", "out of inner loop");
+					date = datelist[ ( datelist[date] or 0 ) - 1];
+					-- log("debug", "next date is %s", date or "EOF");
+				end
+				-- log("debug", "out of outer loop");
+			end);
+
+		else
+
+			-- log("debug", "find start date");
+			while not datelist[date] and date <= end_date do
+				date = tomorrow(date);
+				-- log("debug", "datelist[%q]", date);
+			end
+
+			return coroutine.wrap(function ()
+				local filename, fh, err, len, pos, meta, data, when;
+				-- log("debug", "enter outer loop");
+				-- log("debug", "date = %s", tostring(date));
+				while date and datelist[date] and date <= end_date do
+
+					filename = getpath(user, store, date) .. suffix;
+					-- log("debug", "filename = %q", filename);
+					fh, err = io_open(filename, "rb");
+					if not fh then
+						log("warn", err);
+					else
+						if seek_once then
+							fh:seek("set", seek_once);
+							len = fh:read("*l");
+							fh:seek("cur", tonumber(len) + 1);
+							len = fh:read("*l");
+							fh:seek("cur", tonumber(len) + 1);
+							fh:read("*l");
+							seek_once = nil;
+						end
+						-- pos = fh:seek();
+						len = fh:read("*l");
+						-- log("debug", "enter inner loop");
+						while tonumber(len) do
+							-- log("debug", "len is %s:%s", type(len), tostring(len));
+							meta = fh:read(tonumber(len));
+							fh:seek("cur", 1);
+							-- log("debug", "meta is %s:%s", type(meta), tostring(meta));
+							meta = meta and decode(meta);
+							if not meta then
+								-- log("debug", "no metadata?");
+								break;
+							end
+							len = fh:read("*l");
+							-- log("debug", "{ %q, %s, %q, %q } + #%d", meta[1], meta[2], meta[3], meta[4], len);
+							when = meta[2];
+							if type(when) == "string" then
+								when = datetime.parse(when);
+							end
+							-- log("debug", "when = %s", tostring(when))
+							-- log("debug", "start = %s", tostring(start))
+							-- log("debug", "ending = %s", tostring(ending))
+							if when >= start and when <= ending and (not query.with or meta[3] == query.with) and not seek_once then
+								-- log("debug", "len is %s:%s", type(len), tostring(len));
+								data = fh:read(tonumber(len));
+								fh:seek("cur", 1); fh:read("*l"); -- Go past the trailing length line
+								-- log("debug", "data is %s:%s", type(data), tostring(data));
+								data = decode(data);
+								coroutine.yield(meta[1], (unfreeze[ meta[4] ] or id)(data), when, meta[3]);
+								results = results + 1;
+								if limit and results >= limit then
+									-- log("debug", "results => limit");
+									return;
+								end 
+							else
+								fh:seek("cur", tonumber(len)+1);
+								-- log("debug", "%q", fh:read"*l");
+							end
+							if seek_once then seek_once = nil; end
+							len = fh:read"*l";
+							-- log("debug", "len = %s", tostring(len));
+						end
+					end
+
+					date = datelist[ ( datelist[date] or -1 ) + 1];
+					-- log("debug", "next date is %s", date or "EOF");
+				end
+				-- log("debug", "out of outer loop");
+			end);
+		end
+	end
+
+	function driver:delete(user, query)
+		user = user or "@";
+		if query and query.with then
+			return nil, "not-implemented"; -- More complicated
+		end
+		query = query or NULL;
+		local dstart, dend = "0000-00-00", "9999-99-99";
+		if query["start"] then
+			dstart = datetime.date(query["start"]);
+		end
+		if query["end"] then
+			dend = datetime.date(query["end"]);
+		end
+
+		for i, date in ipairs(get_datelist(user)) do
+			if date > dstart and date < dend then
+				os_remove(getpath(user, store, date) .. suffix);
+			end
+		end
+
+		-- The expensive part
+		local dates, changed;
+		for key in index_storage:keys(user) do
+			changed = false;
+			dates = index_storage:get(user, key);
+			for date in pairs(dates) do
+				if date > dstart and date < dend then
+					dates[date] = nil;
+					changed = true;
+				end
+			end
+			if changed then
+				if next(dates) == nil then dates = nil; end
+				index_storage:set(user, key, dates);
+			end
+		end
+		-- driver:_update_index(user)
+	end
+	return driver;
+end
+
+local drivers = {
+	keyval = new_keyval_store;
+	-- map = new_map_store;
+	archive = new_archive_store;
+};
+
+local pseudomaps = {
+	private = module:get_host_type() == "local", -- Private XML storage
+	persistent = module:get_host_type() == "component", -- Room persistence
+};
+
+function open(_, store, typ)
+	typ = typ or "keyval";
+	-- log("debug", "open(%q, %q, %q)", module.name, store, typ);
+	if pseudomaps[store] then
+		return new_map_store(store, typ == "keyval");
+	end
+	if not drivers[typ] then
+		return nil, "unsupported-store";
+	end
+	return drivers[typ](store);
+end
+
+local function rm(path)
+	did_mkdir_already[path] = nil;
+	os_remove(path);
+end
+
+function purge(_, user)
+	for item in lfs.dir(getpath(user)) do
+		local filename = getpath(user, item);
+		local mode = lfs.attributes(filename, "mode");
+		if mode == "file" and item:sub(-#suffix) == suffix then
+			rm(filename);
+		elseif mode == "directory" and item ~= "." and item ~= ".." then
+			-- map or archive
+			for mapitem in lfs.dir(getpath(user, item)) do
+				local filename = getpath(user, item, mapitem);
+				local mode = lfs.attributes(filename, "mode");
+				if mode == "file" and mapitem:sub(-#suffix) == suffix then
+					rm(filename);
+				end
+			end
+			rm(filename);
+		end
+	end
+	return true;
+end
+
+module:provides("storage");
+
--- a/mod_storage_memory/mod_storage_memory.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_storage_memory/mod_storage_memory.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -19,8 +19,104 @@
 	return true;
 end
 
+local map_store = {};
+map_store.__index = map_store;
+
+function map_store:get(username, key)
+	local userstore = self.store[username];
+	if type(userstore) == "table" then
+		return userstore[key];
+	end
+end
+
+function map_store:set(username, key, data)
+	local userstore = self.store[username];
+	if userstore == nil then
+		userstore = {};
+		self.store[username] = userstore;
+	end
+	userstore[key] = data;
+	return true;
+end
+
+local archive_store = {};
+archive_store.__index = archive_store;
+
+function archive_store:append(username, key, when, with, value)
+	local a = self.store[username];
+	if not a then
+		a = {};
+		self.store[username] = a;
+	end
+	local i = #a+1;
+	local v = { key = key, when = when, with = with, value = value };
+	if not key then
+		key = tostring(a):match"%x+$"..tostring(v):match"%x+$";
+		v.key = key;
+	end
+	a[i] = v;
+	a[key] = i;
+	return true;
+end
+
+function archive_store:find(username, query)
+	local a = self.store[username] or {};
+	local start, stop, step = 1, #a, 1;
+	if query then
+		if query.reverse then
+			start, stop, step = stop, start, -1;
+			if query.before then
+				start = a[query.before];
+			end
+		elseif query.after then
+			start = a[query.after];
+		end
+	end
+	if not start then return nil, "invalid-key";
+	local iter = coroutine.wrap(function (a, start, stop, step, when_start, when_end, match_with)
+		local item, when, with;
+		for i = start, stop, step do
+			item = a[i];
+			when, with = item.when, item.with;
+			if when >= when_start and when_end >= when and (not match_with or match_with == with) then
+				coroutine.yield(item.key, item.value, when, with);
+			end
+		end
+	end);
+	iter(a, start, stop, step, query and query.start or 0, query and query["end"] or math.huge, query and query.with);
+	return iter;
+end
+
+function archive_store:delete(username, query)
+	if not query then
+		self.store[username] = nil;
+		return true;
+	end
+	local old = self.store[username];
+	if not old then return true; end
+	local qstart = query.start or 0;
+	local qend = query["end"] or math.huge;
+	local with = query.with;
+	local new = {};
+	self.store[username] = new;
+	local t;
+	for i = 1, #old do
+		i = old[i];
+		t = i.when;
+		if not(qstart >= t and qend <= t and (not with or i.with == with)) then
+			self:append(username, i.key, t, i.with, i.value);
+		end
+	end
+	if #new == 0 then
+		self.store[username] = nil;
+	end
+	return true;
+end
+
 local stores = {
 	keyval = keyval_store;
+	map = map_store;
+	archive = nil;
 }
 
 local driver = {};
--- a/mod_storage_mongodb/mod_storage_mongodb.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_storage_mongodb/mod_storage_mongodb.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,5 +1,8 @@
 local next = next;
 local setmetatable = setmetatable;
+local set = require"util.set";
+local it = require"util.iterators";
+local array = require"util.array";
 
 local params = assert ( module:get_option("mongodb") , "mongodb configuration not found" );
 
@@ -46,9 +49,79 @@
 	end;
 end
 
+local roster_store = {};
+roster_store.__index = roster_store;
+
+function roster_store:get(username)
+	local host = module.host or "_global";
+	local store = self.store;
+
+	-- The database name can't have a period in it (hence it can't be a host/ip)
+	local namespace = params.dbname .. "." .. host;
+	local v = { _id = { store = store ; username = username } };
+
+	local cursor , err = conn:query ( namespace , v );
+	if not cursor then return nil , err end;
+
+	local r , err = cursor:next ( );
+	if not r then return nil , err end;
+	local roster = {
+		[false] = {
+			version = r.version;
+			pending = setmetatable(set.new( r.pending )._items, nil);
+		};
+	};
+	local items = r.items;
+	for i = 1, #items do
+		local item = items[i];
+		roster[item.jid] = {
+			subscription = item.subscription;
+			groups = set.new( item.groups )._items;
+			ask = item.ask;
+			name = item.name;
+		}
+	end
+	return roster;
+end
+
+function roster_store:set(username, data)
+	local host = module.host or "_global";
+	local store = self.store;
+
+	-- The database name can't have a period in it (hence it can't be a host/ip)
+	local namespace = params.dbname .. "." .. host;
+	local v = { _id = { store = store ; username = username } };
+
+	if data == nil or next(data) == nil then -- delete data
+		return conn:remove ( namespace , v );
+	end
+
+	for k,v in pairs(data[false]) do
+		v[k]=v;
+	end
+	v.pending = array(it.keys(v.pending or data.pending));
+
+	local items  = {}
+	for jid, item in pairs(data) do
+		if jid and jid ~=  "pending" then
+			table.insert(items, {
+				jid = jid;
+				subscription = item.subscription;
+				groups = array(it.keys( item.groups ));
+				name = item.name;
+				ask = item.ask;
+			});
+		end
+	end
+	v.items = items;
+
+	return conn:insert ( namespace , v );
+end
+
 local driver = {};
 
 function driver:open(store, typ)
+	typ = typ or "keyval";
 	if not conn then
 		conn = assert ( mongo.Connection.New ( true ) );
 		assert ( conn:connect ( params.server ) );
@@ -57,8 +130,12 @@
 		end
 	end
 
-	if not typ then -- default key-value store
+	if typ == "keyval" then -- default key-value store
+		if store == "roster" then
+			return setmetatable({ store = store }, roster_store);
+		end
 		return setmetatable({ store = store }, keyval_store);
+		-- TODO archives?
 	end;
 	return nil, "unsupported-store";
 end
--- a/mod_storage_muc_log/mod_storage_muc_log.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_storage_muc_log/mod_storage_muc_log.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,3 +1,6 @@
+-- Copyright (C) 2014 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
 
 local datamanager = require"core.storagemanager".olddm;
 local xml_parse = require"util.xml".parse;
@@ -147,6 +150,25 @@
 	end);
 end
 
+function driver:delete(node, query)
+	local start_date = query and query.start and os_date(datef, query.start) or "000000";
+	local end_date = query and query["end"] and os_date(datef, query["end"]) or "999999";
+
+	local path = datamanager.getpath(node, host, datastore):match("(.*)/");
+	local ok, iter, state, var = pcall(lfs.dir, path);
+	if not ok then
+		module:log("warn", iter);
+		return nil, iter;
+	end
+
+	for dir in iter, state, var do
+		if dir > start_date and dir < end_date then
+			data_store(node, host, datastore .. "/" .. dir, nil);
+		end
+	end
+	return true;
+end
+
 function open(_, store, typ)
 	if typ ~= "archive" then
 		return nil, "unsupported-store";
@@ -154,4 +176,6 @@
 	return setmetatable({ store = store, type = typ }, driver_mt);
 end
 
+purge = driver.delete;
+
 module:provides "storage";
--- a/mod_storage_multi/mod_storage_multi.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_storage_multi/mod_storage_multi.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -1,7 +1,6 @@
 -- mod_storage_multi
 
 local storagemanager = require"core.storagemanager";
-local backends = module:get_option_array(module.name); -- TODO better name?
 
 -- TODO migrate data "upwards"
 
@@ -17,7 +16,7 @@
 	local backends = self.backends;
 	local data, err;
 	for i = 1, #backends do
-		module:log("debug", "%s:%s:get(%q)", tostring(backends[i].get), backends[i]._store, username);
+		module:log("debug", "%s:%s:get(%q)", tostring(backends[i].get), tostring(backends[i]._store), username or "");
 		data, err = backends[i]:get(username);
 		if err then
 			module:log("error", tostring(err));
@@ -48,7 +47,7 @@
 		all = all and ok; -- All successful
 	end
 	if policy == "all" then
-		return all, err
+		return all, err;
 	elseif policy == "majority" then
 		return oks > (#backends/2), err;
 	end
@@ -64,9 +63,10 @@
 
 function driver:open(store, typ)
 	local store_mt = stores[typ or "keyval"];
+	local backends = module:get_option(module.name .. "_backends_".. store) or  module:get_option(module.name .. "_backends");
 	if store_mt then
 		local my_backends = {};
-		local driver, opened
+		local driver, opened;
 		for i = 1, #backends do
 			 driver = storagemanager.load_driver(module.host, backends[i]);
 			 opened = driver:open(store, typ);
@@ -74,6 +74,9 @@
 			 my_backends[i]._store = store;
 		end
 		return setmetatable({ backends = my_backends }, store_mt);
+	elseif backends then
+		module:log("warn", "Unsupported store type %s, passing through to mod_storage_%s", typ, backends[1]);
+		return storagemanager.load_driver(module.host, backends[1]):open(store, typ);
 	end
 	return nil, "unsupported-store";
 end
--- a/mod_strict_https/mod_strict_https.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_strict_https/mod_strict_https.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -20,15 +20,17 @@
 	end
 
 	_old_fire_event = http_server._events.fire_event;
-	function http_server._events.fire_event(event, payload)
-		local request = payload.request;
-		local host = event:match("^[A-Z]+ ([^/]+)");
-		local module = modules[host];
-		if module and not request.secure then
-			payload.response.headers.location = module:http_url(request.path);
-			return 301;
+	if module:get_option_boolean("hsts_redirect", true) then
+		function http_server._events.fire_event(event, payload)
+			local request = payload.request;
+			local host = event:match("^[A-Z]+ ([^/]+)");
+			local module = modules[host];
+			if module and not request.secure then
+				payload.response.headers.location = module:http_url(request.path);
+				return 301;
+			end
+			return _old_fire_event(event, payload);
 		end
-		return _old_fire_event(event, payload);
 	end
 end
 function module.unload()
--- a/mod_throttle_presence/mod_throttle_presence.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_throttle_presence/mod_throttle_presence.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -11,17 +11,17 @@
 	local buffer = session.presence_buffer;
 	local from = stanza.attr.from;
 	if stanza.name ~= "presence" or (stanza.attr.type and stanza.attr.type ~= "unavailable") then
-		local cached_presence = buffer[stanza.attr.from];
+		local cached_presence = buffer[from];
 		if cached_presence then
 			module:log("debug", "Important stanza for %s from %s, flushing presence", session.full_jid, from);
 			stanza._flush = true;
 			cached_presence._flush = true;
 			session.send(cached_presence);
-			buffer[stanza.attr.from] = nil;
+			buffer[from] = nil;
 		end
 	else
-		module:log("debug", "Buffering presence stanza from %s to %s", stanza.attr.from, session.full_jid);
-		buffer[stanza.attr.from] = st.clone(stanza);
+		module:log("debug", "Buffering presence stanza from %s to %s", from, session.full_jid);
+		buffer[from] = st.clone(stanza);
 		return nil; -- Drop this stanza (we've stored it for later)
 	end
 	return stanza;
--- a/mod_vjud/vcard.lib.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_vjud/vcard.lib.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -6,17 +6,21 @@
 
 -- TODO
 -- Fix folding.
+-- vcard4
 
 local st = require "util.stanza";
+local json = require"util.json";
 local t_insert, t_concat = table.insert, table.concat;
 local type = type;
 local next, pairs, ipairs = next, pairs, ipairs;
 
 local from_text, to_text, from_xep54, to_xep54;
+local from_json, to_json;
 
 local line_sep = "\n";
 
 local vCard_dtd; -- See end of file
+local vCard4_dtd;
 
 local function fold_line()
 	error "Not implemented" --TODO
@@ -142,7 +146,7 @@
 			vCards[#vCards+1] = c;
 		elseif name == "END" and value == "VCARD" then
 			c = nil;
-		elseif vCard_dtd[name] then
+		elseif c and vCard_dtd[name] then
 			local dtd = vCard_dtd[name];
 			local p = { name = name };
 			c[#c+1]=p;
@@ -319,6 +323,108 @@
 	end
 end
 
+local json_typs = { string = "text" }
+local function json_type(val)
+	local typ = type(val);
+	if typ == "number" then
+		if val % 1 == val then
+			return "integer";
+		else
+			return "float";
+		end
+	end
+	return json_typs[typ] or json.null;
+end
+
+local function vcard_to_json(vCard)
+	local r = {"vcard"};
+	for i = 1,#vCard do
+		local p = {};
+		local ri = { vCard[i].name:lower(), p, json_type(vCard[i][1]), unpack(vCard[i]) };
+		for k,v in pairs(vCard[i]) do
+			if tostring(k) == k and k ~= "name" then
+				p[k] = v;
+			end
+		end
+		r[1+i] = ri;
+	end
+	return r;
+end
+
+function to_json(vCards)
+	if vCards[1] and vCards[1].name then
+		return json.encode(vcard_to_json(vCards));
+	else
+		local t = {};
+		for i=1,#vCards do
+			t[i]=vcard_to_json(vCards[i]);
+		end
+		return json.encode(t);
+	end
+end
+
+local vcard4 = { }
+
+function vcard4:text(node, params, value)
+	self:tag(node:lower())
+	-- FIXME params
+	if type(value) == "string" then
+		self:tag("text"):text(value):up()
+	elseif vcard4[node] then
+		vcard4[node](value);
+	end
+	self:up();
+end
+
+function vcard4.N(value)
+	for i, k in ipairs(vCard_dtd.N.values) do
+		value:tag(k):text(value[i]):up();
+	end
+end
+
+local xmlns_vcard4 = "urn:ietf:params:xml:ns:vcard-4.0"
+
+local function item_to_vcard4(item)
+	local typ = item.name:lower();
+	local t = st.stanza(typ, { xmlns = xmlns_vcard4 });
+
+	local prop_def = vCard4_dtd[typ];
+	if prop_def == "text" then
+		t:tag("text"):text(item[1]):up();
+	elseif type(prop_def) == "table" then
+		if prop_def.values then
+			for i, v in ipairs(prop_def.values) do
+				t:tag(v:lower()):text(item[i] or ""):up();
+			end
+		else
+			t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
+		end
+	else
+		t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
+	end
+	return t;
+end
+
+local function vcard_to_vcard4xml(vCard)
+	local t = st.stanza("vcard", { xmlns = xmlns_vcard4 });
+	for i=1,#vCard do
+		t:add_child(item_to_vcard4(vCard[i]));
+	end
+	return t;
+end
+
+local function vcards_to_vcard4xml(vCards)
+	if not vCards[1] or vCards[1].name then
+		return vcard_to_vcard4xml(vCards)
+	else
+		local t = st.stanza("vcards", { xmlns = xmlns_vcard4 });
+		for i=1,#vCards do
+			t:add_child(vcard_to_vcard4xml(vCards[i]));
+		end
+		return t;
+	end
+end
+
 -- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
 vCard_dtd = {
 	VERSION = "text", --MUST be 3.0, so parsing is redundant
@@ -445,6 +551,63 @@
 vCard_dtd.LOGO = vCard_dtd.PHOTO;
 vCard_dtd.SOUND = vCard_dtd.PHOTO;
 
+vCard4_dtd = {
+	source = "uri",
+	kind = "text",
+	xml = "text",
+	fn = "text",
+	n = {
+		values = {
+			"family",
+			"given",
+			"middle",
+			"prefix",
+			"suffix",
+		},
+	},
+	nickname = "text",
+	photo = "uri",
+	bday = "date-and-or-time",
+	anniversary = "date-and-or-time",
+	gender = "text",
+	adr = {
+		values = {
+			"pobox",
+			"ext",
+			"street",
+			"locality",
+			"region",
+			"code",
+			"country",
+		}
+	},
+	tel = "text",
+	email = "text",
+	impp = "uri",
+	lang = "language-tag",
+	tz = "text",
+	geo = "uri",
+	title = "text",
+	role = "text",
+	logo = "uri",
+	org = "text",
+	member = "uri",
+	related = "uri",
+	categories = "text",
+	note = "text",
+	prodid = "text",
+	rev = "timestamp",
+	sound = "uri",
+	uid = "uri",
+	clientpidmap = "number, uuid",
+	url = "uri",
+	version = "text",
+	key = "uri",
+	fburl = "uri",
+	caladruri = "uri",
+	caluri = "uri",
+};
+
 return {
 	from_text = from_text;
 	to_text = to_text;
@@ -452,6 +615,10 @@
 	from_xep54 = from_xep54;
 	to_xep54 = to_xep54;
 
+	to_json = to_json;
+
+	to_vcard4 = vcards_to_vcard4xml;
+
 	-- COMPAT:
 	lua_to_text = to_text;
 	lua_to_xep54 = to_xep54;
--- a/mod_webpresence/mod_webpresence.lua	Tue Jan 20 11:02:14 2015 +0000
+++ b/mod_webpresence/mod_webpresence.lua	Sun Jan 25 13:04:02 2015 +0100
@@ -32,8 +32,8 @@
       if user_sessions then
         status = user_sessions.top_resources[1];
         if status and status.presence then
-          message = status.presence:child_with_name("status");
-          status = status.presence:child_with_name("show");
+          message = status.presence:get_child("status");
+          status = status.presence:get_child("show");
           if not status then
             status = "online";
           else