changeset 1777:c353acd1d366

Merge with Goffi
author Kim Alvefur <zash@zash.se>
date Mon, 10 Aug 2015 21:13:31 +0200
parents fb2b9a2e2316 (diff) e7b5ab44339c (current diff)
children 32604bf33a4c
files
diffstat 23 files changed, 730 insertions(+), 114 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_adhoc_blacklist/mod_adhoc_blacklist.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,90 @@
+-- mod_adhoc_blacklist
+--
+-- http://xmpp.org/extensions/xep-0133.html#edit-blacklist
+--
+-- Copyright (C) 2015 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+--
+
+module:depends("adhoc");
+local adhoc = module:require "adhoc";
+local st = require"util.stanza";
+local set = require"util.set";
+local dataform = require"util.dataforms";
+local adhoc_inital_data = require "util.adhoc".new_initial_data_form;
+
+local blocklist_form = dataform.new {
+	title = "Editing the Blacklist";
+	instructions = "Fill out this form to edit the list of entities with whom communications are disallowed.";
+	{
+		type = "hidden";
+		name = "FORM_TYPE";
+		value = "http://jabber.org/protocol/admin";
+	};
+	{
+		type = "jid-multi";
+		name = "blacklistjids";
+		label = "The blacklist";
+	};
+}
+
+local blocklists = module:open_store("blocklist");
+
+local blocklist_handler = adhoc_inital_data(blocklist_form, function ()
+	local blacklistjids = {};
+	local blacklist = blocklists:get();
+	if blacklist then
+		for jid in pairs(blacklist) do
+			table.insert(blacklistjids, jid);
+		end
+	end
+	return { blacklistjids = blacklistjids };
+end, function(fields, form_err)
+	if form_err then
+		return { status = "completed", error = { message = "Problem in submitted form" } };
+	end
+	local blacklistjids = set.new(fields.blacklistjids);
+	local ok, err = blocklists:set(nil, blacklistjids._items);
+	if ok then
+		return { status = "completed", info = "Blacklist updated" };
+	else
+		return { status = "completed", error = { message = "Error saving blacklist: "..err } };
+	end
+end);
+
+module:add_item("adhoc", adhoc.new("Edit Blacklist", "http://jabber.org/protocol/admin#edit-blacklist", blocklist_handler, "admin"));
+
+local function is_blocked(host)
+	local blacklistjids = blocklists:get();
+	return blacklistjids and blacklistjids[host];
+end
+
+module:hook("route/remote", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	if is_blocked(event.to_host) then
+		if origin and stanza then
+			origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Communication with this domain is not allowed"));
+			return true;
+		end
+		return false;
+	end
+end, 1000);
+
+
+module:hook("s2s-stream-features", function (event)
+	local session = event.origin;
+	if is_blocked(session.from_host) then
+		session:close("policy-violation");
+		return false;
+	end
+end, 1000);
+
+module:hook("stanza/http://etherx.jabber.org/streams:features", function (event)
+	local session = event.origin;
+	if is_blocked(session.to_host) then
+		session:close("policy-violation");
+		return true;
+	end
+end, 1000);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_admin_blocklist/mod_admin_blocklist.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,59 @@
+-- mod_admin_blocklist
+--
+-- If a local admin has blocked a domain, don't allow s2s to that domain
+--
+-- Copyright (C) 2015 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+--
+
+module:depends("blocklist");
+
+local st = require"util.stanza";
+local jid_split = require"util.jid".split;
+
+local admins = module:get_option_inherited_set("admins", {}) /
+	function (admin) -- Filter out non-local admins
+		local user, host = jid_split(admin);
+		if host == module.host then return user; end
+	end
+
+local blocklists = module:open_store("blocklist");
+
+local function is_blocked(host)
+	for admin in admins do
+		local blocklist = blocklists:get(admin);
+		if blocklist and blocklist[host] then
+			return true;
+		end
+	end
+end
+
+module:hook("route/remote", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	if is_blocked(event.to_host) then
+		if origin and stanza then
+			origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Communication with this domain is not allowed"));
+			return true;
+		end
+		return false;
+	end
+end, 1000);
+
+
+module:hook("s2s-stream-features", function (event)
+	local session = event.origin;
+	if is_blocked(session.from_host) then
+		session:close("policy-violation");
+		return false;
+	end
+end, 1000);
+
+module:hook("stanza/http://etherx.jabber.org/streams:features", function (event)
+	local session = event.origin;
+	if is_blocked(session.to_host) then
+		session:close("policy-violation");
+		return true;
+	end
+end, 1000);
+
--- a/mod_auth_http_async/mod_auth_http_async.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_auth_http_async/mod_auth_http_async.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_benchmark_storage/mod_benchmark_storage.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,119 @@
+-- mod_benchmark_storage
+-- Copyright (C) 2015 Kim Alvefur
+--
+-- Prime numbers are pretty cool
+
+local gettime = require"socket".gettime;
+
+local sm = require"core.storagemanager";
+local um = require"core.usermanager";
+local mm = require"core.modulemanager";
+
+local test_data, test_users;
+
+function module.command(arg)
+	local test_driver = arg[1];
+	if not test_driver then
+		return print("Usage: prosodyctl mod_"..module.name.." <storage driver>");
+	end
+
+	sm.initialize_host("localhost");
+	um.initialize_host("localhost");
+
+	local start_time = gettime();
+	local storage = assert(sm.load_driver("localhost", test_driver));
+	storage = assert(storage:open("benchmark"));
+	-- for i = 1, 23 do
+		-- storage:set(test_users[i], test_data);
+	-- end
+	local floor, sin, random, pi = math.floor, math.sin, math.random, math.pi;
+	for i = 1, 10079 do
+		if i % 11 == 1 then
+			storage:set(test_users[i%23+1], test_data[3-floor(sin(random()*pi)*3)]);
+		else
+			storage:get(test_users[i%23+1]);
+		end
+		if i % 151 == 0 then
+			-- Give indication of progress
+			io.write("*");
+			io.flush();
+		end
+	end
+	-- Cleanup
+	for i = 1, 23 do
+		storage:set(test_users[i], nil);
+	end
+	mm.unload("localhost", "storage_"..test_driver);
+	local time_taken = gettime() - start_time;
+	io.write("\27[0G\27[K"); -- Clear current line
+	io.flush();
+	print(("Took %fs with mod_storage_%s"):format(time_taken, test_driver));
+end
+
+-- 23 usernames
+test_users = {
+	"tritonymph"; "ankylotomy"; "tron"; "barbaric"; "twiddler"; 
+	"spiritful"; "unmollifiably"; "suggestion"; "presubsistence"; 
+	"unneeded"; "taxemic"; "teloteropathic"; "nonbending"; "mev"; 
+	"septifragally"; "clame"; "obsolescent"; "unconceivable"; 
+	"foolishly"; "conjunctur"; "precirculation"; "bethump"; "vermivorous";
+};
+
+test_data = {
+	{ some_data = "tiny data" };
+	--
+	{ [false] = { version = 1; pending = {}; }; -- Medium data
+		["user@example.com"] = { subscription = "both"; groups = {};
+			name = "My Best Friend"; }; };
+	--
+	{ attr = { xmlns = "vcard-temp"; }; name = "vCard"; -- The largest data
+	{ attr = { xmlns = "vcard-temp"; }; name = "NICKNAME"; "Buster"; };
+	{ attr = { xmlns = "vcard-temp"; }; name = "PHOTO";
+	{ attr = { xmlns = "vcard-temp"; }; name = "TYPE"; "image/jpeg"; };
+	{ attr = { xmlns = "vcard-temp"; }; name = "BINVAL"; [[
+	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
+	FsQtGrr65p97R/X/ANxCn6r/2Q==]]; }; }; };
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compact_resource/mod_compact_resource.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,12 @@
+
+local base64_encode = require"util.encodings".base64.encode;
+local random_bytes = require"util.random".bytes;
+
+local b64url = { ["+"] = "-", ["/"] = "_", ["="] = "" };
+local function random_resource()
+	return base64_encode(random_bytes(8)):gsub("[+/=]", b64url);
+end
+
+module:hook("pre-resource-bind", function (event)
+	event.resource = random_resource();
+end);
--- a/mod_filter_chatstates/mod_filter_chatstates.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_filter_chatstates/mod_filter_chatstates.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -1,9 +1,6 @@
 local filters = require "util.filters";
 local st = require "util.stanza";
 
-local dummy_stanza_mt = setmetatable({ __tostring = function () return ""; end }, { __index = st.stanza_mt });
-local dummy_stanza = setmetatable(st.stanza(), dummy_stanza_mt);
-
 module:depends("csi");
 
 local function filter_chatstates(stanza)
@@ -11,11 +8,11 @@
 		stanza = st.clone(stanza);
 		stanza:maptags(function (tag)
 			if tag.attr.xmlns ~= "http://jabber.org/protocol/chatstates" then
-				return tag;
+				return tag
 			end
 		end);
 		if #stanza.tags == 0 then
-			return dummy_stanza;
+			return nil;
 		end
 	end
 	return stanza;
--- a/mod_http_muc_log/http_muc_log.html	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_http_muc_log/http_muc_log.html	Mon Aug 10 21:13:31 2015 +0200
@@ -44,7 +44,7 @@
 <header>
 <h1 title="xmpp:{jid?}">{title}</h1>
 <nav>{links#
-<a class="{rel?}" href="{href}" rel="{rel?}">{text}</a>}
+<a class="{item.rel?}" href="{item.href}" rel="{item.rel?}">{item.text}</a>}
 </nav>
 </header>
 <hr>
@@ -52,33 +52,33 @@
 <nav>
 <dl class="room-list">
 {rooms#
-<dt class="name"><a href="{href}">{name}</a></dt>
-<dd class="description">{description?}</dd>}
+<dt class="name"><a href="{item.href}">{item.name}</a></dt>
+<dd class="description">{item.description?}</dd>}
 </dl>
 {years#
-<h2 id="{year}">{year}</h2>
-{months#
-<table id="{month}-{year}">
-<caption>{month}</caption>
-<tr><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th></tr>{weeks#
-<tr>{days#<td>{href&<a href="{href}">}{day?&nbsp;}{href&</a>}</td>}</tr>}
+<h2 id="{item.year}">{item.year}</h2>
+{item.months#
+<table id="{item.month}-{item.year}">
+<caption>{item.month}</caption>
+<tr><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th></tr>{item.weeks#
+<tr>{item.days#<td>{item.href&<a href="{item.href}">}{item.day?&nbsp;}{item.href&</a>}</td>}</tr>}
 </table>
 }
 }
 </nav>
 <ol class="chat-logs">{lines#
-<li class="{st_name} {st_type?}" id="{key}">
-<a class="time" href="#{key}"><time datetime="{datetime}">{time}</time></a>
-<b class="nick">{nick}</b>
-<em class="verb">{verb?}</em>
-<q class="body">{body?}</q>
+<li class="{item.st_name} {item.st_type?}" id="{item.key}">
+<a class="time" href="#{item.key}"><time datetime="{item.datetime}">{item.time}</time></a>
+<b class="nick">{item.nick}</b>
+<em class="verb">{item.verb?}</em>
+<q class="body">{item.body?}</q>
 </li>}
 </ol>
 </div>
 <hr>
 <footer>
 <nav>{links#
-<a class="{rel?}" href="{href}" rel="{rel?}">{text}</a>}
+<a class="{item.rel?}" href="{item.href}" rel="{item.rel?}">{item.text}</a>}
 </nav>
 <br>
 <div class="powered-by">Prosody</div>
--- a/mod_http_muc_log/mod_http_muc_log.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_http_muc_log/mod_http_muc_log.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -1,4 +1,3 @@
-local st = require "util.stanza";
 local mt = require"util.multitable";
 local datetime = require"util.datetime";
 local jid_split = require"util.jid".split;
@@ -7,9 +6,8 @@
 local it = require"util.iterators";
 local gettime = require"socket".gettime;
 local url = require"socket.url";
-local xml_escape = st.xml_escape;
-local t_concat = table.concat;
 local os_time, os_date = os.time, os.date;
+local render = require"util.interpolation".new("%b{}", require"util.stanza".xml_escape);
 
 local archive = module:open_store("muc_log", "archive");
 
@@ -33,45 +31,6 @@
 
 module:depends"http";
 
-local function render(template, values)
-	-- This function takes a string template and a table of values.
-	-- Sequences like {name} in the template string are substituted
-	-- with values from the table, optionally depending on a modifier
-	-- symbol.
-	--
-	-- Variants are:
-	-- {name} is substituted for values["name"] and is XML escaped
-	-- {name? sub-template } renders a sub-template if values["name"] is false-ish
-	-- {name& sub-template } renders a sub-template if values["name"] is true-ish
-	-- {name# sub-template } renders a sub-template using an array of values
-	-- {name!} is substituted *without* XML escaping
-	return (template:gsub("%b{}", function (block)
-		local name, opt, e = block:sub(2, -2):match("^([%a_][%w_]*)(%p?)()");
-		local value = values[name];
-		if opt == '#' then
-			if not value or not value[1] then return ""; end
-			local out, subtpl = {}, block:sub(e+1, -2);
-			for i=1, #value do
-				out[i] = render(subtpl, value[i]);
-			end
-			return t_concat(out);
-		elseif opt == '&' then
-			if not value then return ""; end
-			return render(block:sub(e+1, -2), values);
-		elseif opt == '?' and not value then
-			return render(block:sub(e+1, -2), values);
-		elseif value ~= nil then
-			if type(value) ~= "string" then
-				value = tostring(value);
-			end
-			if opt ~= '!' then
-				return xml_escape(value);
-			end
-			return value;
-		end
-	end));
-end
-
 local template;
 do
 	local template_file = module:get_option_string(module.name .. "_template", module.name .. ".html");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_http_upload/mod_http_upload.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,110 @@
+-- mod_http_upload
+--
+-- Copyright (C) 2015 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+-- 
+-- Implementation of HTTP Upload file transfer mechanism used by Conversations
+--
+
+-- imports
+local st = require"util.stanza";
+local lfs = require"lfs";
+local join_path = require"util.paths".join;
+local uuid = require"util.uuid".generate;
+
+-- depends
+module:depends("http");
+
+-- namespace
+local xmlns_http_upload = "eu:siacs:conversations:http:upload";
+
+module:add_feature(xmlns_http_upload);
+
+-- state
+local pending_slots = module:shared("upload_slots");
+
+local storage_path = join_path(prosody.paths.data, module.name);
+lfs.mkdir(storage_path);
+
+-- hooks
+module:hook("iq/host/"..xmlns_http_upload..":request", function (event)
+	local stanza, origin = event.stanza, event.origin;
+	-- local clients only
+	if origin.type ~= "c2s" then
+		origin.send(st.error_reply(stanza, "cancel", "not-authorized"));
+		return true;
+	end
+	-- validate
+	local filename = stanza.tags[1]:get_child_text("filename");
+	if not filename or filename:find("/") then
+		origin.send(st.error_reply(stanza, "modify", "bad-request"));
+		return true;
+	end
+	local reply = st.reply(stanza);
+	reply:tag("slot", { xmlns = xmlns_http_upload });
+	local random = uuid();
+	pending_slots[random.."/"..filename] = origin.full_jid;
+	local url = module:http_url() .. "/" .. random .. "/" .. filename;
+	reply:tag("get"):text(url):up();
+	reply:tag("put"):text(url):up();
+	origin.send(reply);
+	return true;
+end);
+
+-- http service
+local function upload_data(event, path)
+	if not pending_slots[path] then
+		return 401;
+	end
+	local random, filename = path:match("^([^/]+)/([^/]+)$");
+	if not random then
+		return 400;
+	end
+	local dirname = join_path(storage_path, random);
+	if not lfs.mkdir(dirname) then
+		module:log("error", "Could not create directory %s for upload", dirname);
+		return 500;
+	end
+	local full_filename = join_path(dirname, filename);
+	local fh, ferr = io.open(full_filename, "w");
+	if not fh then
+		module:log("error", "Could not open file %s for upload: %s", full_filename, ferr);
+		return 500;
+	end
+	local ok, err = fh:write(event.request.body);
+	if not ok then
+		module:log("error", "Could not write to file %s for upload: %s", full_filename, err);
+		os.remove(full_filename);
+		return 500;
+	end
+	ok, err = fh:close();
+	if not ok then
+		module:log("error", "Could not write to file %s for upload: %s", full_filename, err);
+		os.remove(full_filename);
+		return 500;
+	end
+	module:log("info", "File uploaded by %s to slot %s", pending_slots[path], random);
+	pending_slots[path] = nil;
+	return 200;
+end
+
+local serve_uploaded_files = module:depends("http_files").serve(storage_path);
+
+local function size_only(request, data)
+	request.headers.content_size = #data;
+	return 200;
+end
+
+local function serve_head(event, path)
+	event.send = size_only;
+	return serve_uploaded_files(event, path);
+end
+
+module:provides("http", {
+	route = {
+		["GET /*"] = serve_uploaded_files;
+		["HEAD /*"] = serve_head;
+		["PUT /*"] = upload_data;
+	};
+});
--- a/mod_list_inactive/mod_list_inactive.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_list_inactive/mod_list_inactive.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -19,6 +19,15 @@
 }
 
 function module.command(arg)
+	if #arg < 2 then
+		print("usage: prosodyctl mod_list_inactive example.net time [format]");
+		print("time is a number followed by 'day', 'week', 'month' or 'year'");
+		print("formats are:");
+		for name, fmt in pairs(output_formats) do
+			print(name, fmt:format("user@example.com", "last action"))
+		end
+		return;
+	end
 	local items = {};
 	local host = arg[1];
 	assert(hosts[host], "Host "..tostring(host).." does not exist");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_log_rate/mod_log_rate.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,17 @@
+module:set_global();
+
+local measure = require"core.statsmanager".measure;
+
+local function sink_maker(config)
+	local levels = {
+		debug = measure("rate", "log.debug");
+		info = measure("rate", "log.info");
+		warn = measure("rate", "log.warn");
+		error = measure("rate", "log.error");
+	};
+	return function (_, level)
+		return levels[level]();
+	end
+end
+
+require"core.loggingmanager".register_sink_type("measure", sink_maker);
--- a/mod_mam/mod_mam.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_mam/mod_mam.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -41,7 +41,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
 
@@ -230,7 +230,9 @@
 	-- or that don't have a <body/>
 	or not stanza:get_child("body")
 	-- or if hints suggest we shouldn't
+	or stanza:get_child("no-permanent-storage", "urn:xmpp:hints") -- The XEP needs to decide on "store" or "storage"
 	or stanza:get_child("no-permanent-store", "urn:xmpp:hints")
+	or stanza:get_child("no-storage", "urn:xmpp:hints")
 	or stanza:get_child("no-store", "urn:xmpp:hints") then
 		module:log("debug", "Not archiving stanza: %s (content)", stanza:top_tag());
 		return;
@@ -247,6 +249,9 @@
 
 		-- And stash it
 		local ok, id = archive:append(store_user, nil, time_now(), with, stanza);
+		if ok then
+			module:fire_event("archive-message-added", { origin = origin, stanza = stanza, for_user = store_user, id = id });
+		end
 	else
 		module:log("debug", "Not archiving stanza: %s (prefs)", stanza:top_tag());
 	end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_mamsub/mod_mamsub.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,64 @@
+-- MAM Subscriptions prototype
+-- Copyright (C) 2015 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+
+local mt = require"util.multitable";
+local st = require"util.stanza";
+
+local xmlns_mamsub = "http://prosody.im/protocol/mamsub";
+
+module:add_feature(xmlns_mamsub);
+
+local host_sessions = prosody.hosts[module.host].sessions;
+
+local weak = { __mode = "k" };
+
+module:hook("iq-set/self/"..xmlns_mamsub..":subscribe", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	if origin.mamsub ~= nil then
+		origin.send(st.error_reply(stanza, "modify", "conflict"));
+		return true;
+	end
+	origin.mamsub = xmlns_mamsub;
+	local mamsub_sessions = host_sessions[origin.username].mamsub_sessions;
+	if not mamsub_sessions then
+		mamsub_sessions = setmetatable({}, weak);
+		host_sessions[origin.username].mamsub_sessions = mamsub_sessions;
+	end
+	mamsub_sessions[origin] = true;
+	origin.send(st.reply(stanza));
+	return true;
+end);
+
+module:hook("iq-set/self/"..xmlns_mamsub..":unsubscribe", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	if origin.mamsub ~= xmlns_mamsub then
+		origin.send(st.error_reply(stanza, "modify", "conflict"));
+		return true;
+	end
+	origin.mamsub = nil;
+	local mamsub_sessions = host_sessions[origin.username].mamsub_sessions;
+	if mamsub_sessions then
+		mamsub_sessions[origin] = nil;
+	end
+	origin.send(st.reply(stanza));
+	return true;
+end);
+
+module:hook("archive-message-added", function (event)
+	local user_session = host_sessions[event.for_user];
+	local mamsub_sessions = user_session and user_session.mamsub_sessions;
+	if not mamsub_sessions then return end;
+
+	local for_broadcast = st.message():tag("mamsub", { xmlns = xmlns_mamsub })
+		:tag("forwarded", { xmlns = "urn:xmpp:forward:0" })
+			:add_child(event.stanza);
+
+	for session in pairs(mamsub_sessions) do
+		if session.mamsub == xmlns_mamsub then
+			for_broadcast.attr.to = session.full_jid;
+			session.send(for_broadcast);
+		end
+	end
+end);
--- a/mod_muc_limits/mod_muc_limits.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_muc_limits/mod_muc_limits.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -1,8 +1,8 @@
 
-local rooms = module:shared "muc/rooms";
+local mod_muc = module:depends"muc";
+local rooms = rawget(mod_muc, "rooms"); -- Old MUC API
 if not rooms then
-	module:log("error", "This module only works on MUC components!");
-	return;
+	rooms = module:shared"muc/rooms"; -- New MUC API
 end
 
 local jid_split, jid_bare = require "util.jid".split, require "util.jid".bare;
--- a/mod_profile/mod_profile.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_profile/mod_profile.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -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,18 +87,25 @@
 	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
+		if err then
+			origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+			return true;
+		end
 		data = legacy_storage:get(username);
 		data = data and st.deserialize(data);
 		if data then
-			return origin.send(st.reply(stanza):add_child(data));
+			origin.send(st.reply(stanza):add_child(data));
+			return true;
 		end
 	end
 	if not data then
-		return origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+		origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+		return true;
 	end
-	return origin.send(st.reply(stanza):add_child(vcard.to_xep54(data)));
+	origin.send(st.reply(stanza):add_child(vcard.to_xep54(data)));
+	return true;
 end
 
 local function handle_set(event)
@@ -107,20 +115,23 @@
 	local to = stanza.attr.to;
 	if to then
 		if not is_admin(jid_bare(stanza.attr.from), module.host) then
-			return origin.send(st.error_reply(stanza, "auth", "forbidden"));
+			origin.send(st.error_reply(stanza, "auth", "forbidden"));
+			return true;
 		end
 		username = jid_split(to);
 	end
 	local ok, err = storage:set(username, data);
 	if not ok then
-		return origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+		origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+		return true;
 	end
 
 	if pep_plus and username then
 		update_pep(username, data);
 	end
 
-	return origin.send(st.reply(stanza));
+	origin.send(st.reply(stanza));
+	return true;
 end
 
 module:hook("iq-get/bare/vcard-temp:vCard", handle_get);
@@ -185,9 +196,11 @@
 		local username = jid_split(stanza.attr.to) or origin.username;
 		local data = storage:get(username);
 		if not data then
-			return origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+			origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+			return true;
 		end
-		return origin.send(st.reply(stanza):add_child(vcard.to_vcard4(data)));
+		origin.send(st.reply(stanza):add_child(vcard.to_vcard4(data)));
+		return true;
 	end);
 
 	if vcard.from_vcard4 then
@@ -195,14 +208,17 @@
 			local origin, stanza = event.origin, event.stanza;
 			local ok, err = storage:set(origin.username, vcard.from_vcard4(stanza.tags[1]));
 			if not ok then
-				return origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+				origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
+				return true;
 			end
-			return origin.send(st.reply(stanza));
+			origin.send(st.reply(stanza));
+			return true;
 		end);
 	else
 		module:hook("iq-set/self/urn:ietf:params:xml:ns:vcard-4.0:vcard", function(event)
 			local origin, stanza = event.origin, event.stanza;
-			return origin.send(st.error_reply(stanza, "cancel", "feature-not-implemented"));
+			origin.send(st.error_reply(stanza, "cancel", "feature-not-implemented"));
+			return true;
 		end);
 	end
 end
--- a/mod_s2s_auth_dane/mod_s2s_auth_dane.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_s2s_auth_dane/mod_s2s_auth_dane.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -14,6 +14,8 @@
 -- No encryption offered
 -- Different hostname before and after STARTTLS - mod_s2s should complain
 -- Interaction with Dialback
+--
+-- luacheck: ignore module
 
 module:set_global();
 
@@ -294,7 +296,10 @@
 							log("info", "DANE validated ok for %s using %s", host, tlsa:getUsage());
 							if use == 2 then -- DANE-TA
 								session.cert_identity_status = "valid";
-								session.cert_chain_status = "valid";
+								if cert_verify_identity(host, "xmpp-server", cert) then
+									session.cert_chain_status = "valid";
+									-- else -- TODO Check against SRV target?
+								end
 								-- for usage 0, PKIX-CA, identity and chain has to be valid already
 							end
 							match_found = true;
--- a/mod_smacks/mod_smacks.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_smacks/mod_smacks.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -298,6 +298,18 @@
 	end
 end);
 
+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);
+
 function handle_resume(session, stanza, xmlns_sm)
 	if session.full_jid then
 		session.log("warn", "Tried to resume after resource binding");
@@ -365,3 +377,18 @@
 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;
+	if session.smacks then
+		if session.awaiting_ack then
+			return false; -- Kick the session
+		end
+		(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
+		session.awaiting_ack = true;
+		return true;
+	end
+end
+
+module:hook("s2s-read-timeout", handle_read_timeout);
+module:hook("c2s-read-timeout", handle_read_timeout);
--- a/mod_smacks_offline/mod_smacks_offline.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_smacks_offline/mod_smacks_offline.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -21,9 +21,11 @@
 
 local host_sessions = prosody.hosts[module.host].sessions;
 mod_smacks.handle_unacked_stanzas = function (session)
-	local sessions = host_sessions[session.username].sessions;
-	if next(sessions) == session.resource and next(sessions, session.resource) == nil then
-		store_unacked_stanzas(session)
+	if session.username then
+		local sessions = host_sessions[session.username].sessions;
+		if next(sessions) == session.resource and next(sessions, session.resource) == nil then
+			store_unacked_stanzas(session)
+		end
 	end
 	return handle_unacked_stanzas(session);
 end
--- a/mod_storage_gdbm/mod_storage_gdbm.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_storage_gdbm/mod_storage_gdbm.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -73,7 +73,11 @@
 archive.get = keyval.get;
 archive.set = keyval.set;
 
-function archive:append(username, key, when, with, value)
+function archive:append(username, key, value, when, with)
+	if type(when) ~= "number" then
+		when, with, value = value, when, with;
+	end
+
 	key = key or uuid();
 	local meta = self:get(username);
 	if not meta then
@@ -148,7 +152,7 @@
 		db = assert(gdbm.open(db_path, "c"));
 		cache[db_path] = db;
 	end
-	return setmetatable({ _db = db; _path = db_path; store = store, typ = type }, driver_mt);
+	return setmetatable({ _db = db; _path = db_path; store = store, type = typ }, driver_mt);
 end
 
 function purge(_, user)
@@ -166,6 +170,7 @@
 function module.unload()
 	for db_path, db in pairs(cache) do
 		module:log("debug", "Closing db at %q", db_path);
+		gdbm.reorganize(db);
 		gdbm.sync(db);
 		gdbm.close(db);
 	end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_storage_lmdb/mod_storage_lmdb.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -0,0 +1,86 @@
+-- mod_storage_lmdb
+-- Copyright (C) 2015 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+-- 
+-- Depends on lightningdbm
+-- https://github.com/shmul/lightningdbm
+--
+-- luacheck: globals prosody open
+
+local lmdb = require"lightningmdb";
+local lfs = require"lfs";
+local path = require"util.paths";
+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);
+
+local env = lmdb.env_create();
+assert(env:set_maxdbs(module:get_option_number("lmdb_maxdbs", 20)));
+local env_flags = 0;
+for i, flag in ipairs(module:get_option_array("lmdb_flags", {})) do
+	env_flags = env_flags + assert(lmdb["MDB_"..flag:upper()], "No such flag "..flag);
+end
+env:open(base_path, env_flags, tonumber("640", 8));
+
+local keyval = {};
+local keyval_mt = { __index = keyval, flags = lmdb.MDB_CREATE };
+
+function keyval:set(user, value)
+	local t = self.env:txn_begin(nil, 0);
+	if type(value) == "table" and next(value) == nil then
+		value = nil;
+	end
+	if value ~= nil then
+		value = serialize(value);
+	end
+	local ok, err;
+	if value ~= nil then
+		ok, err = t:put(self.db, user, value, 0);
+	else
+		ok, err = t:del(self.db, user, value);
+	end
+	if not ok then
+		t:abort();
+		return nil, err;
+	end
+	return t:commit();
+end
+
+function keyval:get(user)
+	local t = self.env:txn_begin(nil, 0);
+	local data, err = t:get(self.db, user, 0);
+	if not data then
+		t:abort();
+		return nil, err;
+	end
+	t:commit();
+	return deserialize(data);
+end
+
+local drivers = {
+	keyval = keyval_mt;
+}
+
+function open(_, store, typ)
+	typ = typ or "keyval";
+	local driver_mt = drivers[typ];
+	if not driver_mt then
+		return nil, "unsupported-store";
+	end
+	local t = env:txn_begin(nil, 0);
+	local db = t:dbi_open(store.."_"..typ, driver_mt.flags);
+	assert(t:commit());
+
+	return setmetatable({ env = env, store = store, type = typ, db = db }, driver_mt);
+end
+
+function module.unload()
+	env:sync(1);
+	env:close();
+end
+
+module:provides("storage");
--- a/mod_storage_memory/mod_storage_memory.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_storage_memory/mod_storage_memory.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -42,7 +42,10 @@
 local archive_store = {};
 archive_store.__index = archive_store;
 
-function archive_store:append(username, key, when, with, value)
+function archive_store:append(username, key, value, when, with)
+	if type(when) ~= "number" then
+		when, with, value = value, when, with;
+	end
 	local a = self.store[username];
 	if not a then
 		a = {};
@@ -116,7 +119,7 @@
 		i = old[i];
 		t = i.when;
 		if not(qstart >= t and qend <= t and (not qwith or i.with == qwith)) then
-			self:append(username, i.key, t, i.with, i.value);
+			self:append(username, i.key, i.value, t, i.with);
 		end
 	end
 	if #new == 0 then
--- a/mod_storage_muc_log/mod_storage_muc_log.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_storage_muc_log/mod_storage_muc_log.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -48,7 +48,10 @@
 	return with and tag.name .. "<" .. with or tag.name;
 end
 
-function driver:append(node, key, when, with, stanza)
+function driver:append(node, key, stanza, when, with)
+	if type(when) ~= "number" then
+		when, with, stanza = stanza, when, with;
+	end
 	local today = os_date(datef, when);
 	local now = os_date(timef, when);
 	local data = data_load(node, host, datastore .. "/" .. today) or {};
--- a/mod_storage_xmlarchive/mod_storage_xmlarchive.lua	Fri Jul 31 18:46:27 2015 +0200
+++ b/mod_storage_xmlarchive/mod_storage_xmlarchive.lua	Mon Aug 10 21:13:31 2015 +0200
@@ -1,3 +1,10 @@
+-- mod_storage_xmlarchive
+-- Copyright (C) 2015 Kim Alvefur
+--
+-- This file is MIT/X11 licensed.
+--
+-- luacheck: ignore unused self
+
 local dm = require "core.storagemanager".olddm;
 local hmac_sha256 = require"util.hashes".hmac_sha256;
 local st = require"util.stanza";
@@ -12,9 +19,9 @@
 	if not ok then
 		return ok, msg;
 	end
-	f:seek("set", offset);
-	return true;
+	return f:seek("set", offset);
 end;
+
 pcall(function()
 	local pposix = require "util.pposix";
 	fallocate = pposix.fallocate or fallocate;
@@ -23,32 +30,37 @@
 local archive = {};
 local archive_mt = { __index = archive };
 
-function archive:append(username, _, when, with, data)
+function archive:append(username, _, data, when, with)
+	if type(when) ~= "number" then
+		when, with, data = data, when, with;
+	end
 	if getmetatable(data) ~= st.stanza_mt then
+		module:log("error", "Attempt to store non-stanza object, traceback: %s", debug.traceback());
 		return nil, "unsupported-datatype";
 	end
+
 	username = username or "@";
 	data = tostring(data) .. "\n";
+
 	local day = dt.date(when);
 	local filename = dm.getpath(username.."@"..day, module.host, self.store, "xml", true);
+
 	local ok, err;
 	local f = io.open(filename, "r+");
 	if not f then
-		f, err = io.open(filename, "w");
-		if not f then return nil, err; end
+		f, err = io.open(filename, "w");     if not f then return nil, err; end
 		ok, err = dm.list_append(username, module.host, self.store, day);
 		if not ok then return nil, err; end
 	end
-	local offset = f and f:seek("end");
-	ok, err = fallocate(f, offset, #data);
-	if not ok then return nil, err; end
-	f:seek("set", offset);
-	ok, err = f:write(data);
-	if not ok then return nil, err; end
-	ok, err = f:close();
-	if not ok then return nil, err; end
+
+	local offset = f:seek("end"); -- Seek to the end and collect current file length
+	-- then make sure there is enough free space for what we're about to add
+	ok, err = fallocate(f, offset, #data); if not ok then return nil, err; end
+	ok, err = f:write(data);               if not ok then return nil, err; end
+	ok, err = f:close();                   if not ok then return nil, err; end
+
 	local id = day .. "-" .. hmac_sha256(username.."@"..day.."+"..offset, data, true):sub(-16);
-	ok, err = dm.list_append(username.."@"..day, module.host, self.store, { id = id, when = when, with = with, offset = offset, length = #data });
+	ok, err = dm.list_append(username.."@"..day, module.host, self.store, { id = id, when = dt.datetime(when), with = with, offset = offset, length = #data });
 	if not ok then return nil, err; end
 	return id;
 end
@@ -69,7 +81,13 @@
 	local stream = new_stream(stream_sess, { handlestanza = cb, stream_ns = "jabber:client"});
 	local dates = dm.list_load(username, module.host, self.store) or empty;
 	stream:feed(st.stanza("stream", { xmlns = "jabber:client" }):top_tag());
-	stream_sess.notopen = nil;
+	local function reset_stream()
+		stream:reset();
+		stream_sess.notopen = true;
+		stream:feed(st.stanza("stream", { xmlns = "jabber:client" }):top_tag());
+		stream_sess.notopen = nil;
+	end
+	reset_stream();
 
 	local limit = query.limit;
 	local start_day, step, last_day = 1, 1, #dates;
@@ -100,10 +118,11 @@
 
 	return function ()
 		if limit and count >= limit then xmlfile:close() return; end
+		local filename;
 
 		for d = start_day, last_day, step do
 			if d ~= start_day or not items then
-				module:log("debug", "Load items for %s", dates[d]);
+				module:log("debug", "Loading items from %s", dates[d]);
 				start_day = d;
 				items = dm.list_load(username .. "@" .. dates[d], module.host, self.store) or empty;
 				if not rev then
@@ -112,24 +131,33 @@
 					first_item, last_item = #items, 1;
 				end
 				local ferr;
-				xmlfile, ferr = io.open(dm.getpath(username .. "@" .. dates[d], module.host, self.store, "xml"));
+				filename = dm.getpath(username .. "@" .. dates[d], module.host, self.store, "xml");
+				xmlfile, ferr = io.open(filename);
 				if not xmlfile then
 					module:log("error", "Error: %s", ferr);
 					return;
 				end
 			end
 
+			local q_with, q_start, q_end = query.with, query.start, query["end"];
 			for i = first_item, last_item, step do
-				module:log("debug", "data[%q][%d]", dates[d], i);
 				local item = items[i];
+				local i_when, i_with = item.when, item.with;
+				if type(i_when) == "string" then
+					i_when = dt.parse(i_when);
+				end
+				if type(i_when) ~= "number" then
+					module:log("warn", "data[%q][%d].when is invalid", dates[d], i);
+					break;
+				end
 				if not item then
-					module:log("debug", "data[%q][%d] is nil", dates[d], i);
+					module:log("warn", "data[%q][%d] is nil", dates[d], i);
 					break;
 				end
 				if xmlfile and in_range
-				and (not query.with or item.with == query.with)
-				and (not query.start or item.when >= query.start)
-				and (not query["end"] or item.when <= query["end"]) then
+				and (not q_with or i_with == q_with)
+				and (not q_start or i_when >= q_start)
+				and (not q_end or i_when <= q_end) then
 					count = count + 1;
 					first_item = i + step;
 
@@ -137,12 +165,13 @@
 					local data = xmlfile:read(item.length);
 					local ok, err = stream:feed(data);
 					if not ok then
-						module:log("warn", "Parse error: %s", err);
+						module:log("warn", "Parse error in %s at %d+%d: %s", filename, item.offset, item.length, err);
+						reset_stream();
 					end
 					if result then
 						local stanza = result;
 						result = nil;
-						return item.id, stanza, item.when, item.with;
+						return item.id, stanza, i_when, i_with;
 					end
 				end
 				if (rev and item.id == query.after) or