diff --git a/debian/patches/0001-TT-7482-mod_man-support-mam-version-0.5-urn-xmpp-mam.patch b/debian/patches/0001-TT-7482-mod_man-support-mam-version-0.5-urn-xmpp-mam.patch deleted file mode 100644 index 9a5c85b..0000000 --- a/debian/patches/0001-TT-7482-mod_man-support-mam-version-0.5-urn-xmpp-mam.patch +++ /dev/null @@ -1,189 +0,0 @@ -From 9e65f1029f84b0336f5096f6e56b27cb117ec568 Mon Sep 17 00:00:00 2001 -From: Victor Seva -Date: Mon, 26 Dec 2016 14:42:46 +0100 -Subject: [PATCH] TT#7482 mod_man: support mam version 0.5 (urn:xmpp:mam:1) - -* keep compability with mam version 0.3 (urn:xmpp:mam:0) - -Change-Id: I03dcc49c39b5eb6c38e30d27bcc3b93b8137c770 ---- - plugins/mod_mam/mamprefsxml.lib.lua | 3 +- - plugins/mod_mam/mod_mam.lua | 80 +++++++++++++++++++++++++++++-------- - 2 files changed, 64 insertions(+), 19 deletions(-) - -diff --git a/plugins/mod_mam/mamprefsxml.lib.lua b/plugins/mod_mam/mamprefsxml.lib.lua -index a0d4624..4b3a50b 100644 ---- a/plugins/mod_mam/mamprefsxml.lib.lua -+++ b/plugins/mod_mam/mamprefsxml.lib.lua -@@ -4,7 +4,6 @@ - -- This file is MIT/X11 licensed. - - local st = require"util.stanza"; --local xmlns_mam = "urn:xmpp:mam:0"; - - local default_attrs = { - always = true, [true] = "always", -@@ -12,7 +11,7 @@ local default_attrs = { - roster = "roster", - } - --local function tostanza(prefs) -+local function tostanza(prefs, xmlns_mam) - local default = prefs[false]; - default = default_attrs[default]; - local prefstanza = st.stanza("prefs", { xmlns = xmlns_mam, default = default }); -diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua -index fe69e33..dbdd92a 100644 ---- a/plugins/mod_mam/mod_mam.lua -+++ b/plugins/mod_mam/mod_mam.lua -@@ -3,7 +3,8 @@ - -- - -- This file is MIT/X11 licensed. - --local xmlns_mam = "urn:xmpp:mam:0"; -+local xmlns_mam0 = "urn:xmpp:mam:0"; -+local xmlns_mam1 = "urn:xmpp:mam:1"; - local xmlns_delay = "urn:xmpp:delay"; - local xmlns_forward = "urn:xmpp:forward:0"; - -@@ -54,11 +55,11 @@ end - local cleanup; - - -- Handle prefs. --module:hook("iq/self/"..xmlns_mam..":prefs", function(event) -+local function handle_prefs(event, xmlns_mam) - local origin, stanza = event.origin, event.stanza; - local user = origin.username; - if stanza.attr.type == "get" then -- local prefs = prefs_to_stanza(get_prefs(user)); -+ local prefs = prefs_to_stanza(get_prefs(user), xmlns_mam); - local reply = st.reply(stanza):add_child(prefs); - origin.send(reply); - else -- type == "set" -@@ -72,24 +73,47 @@ module:hook("iq/self/"..xmlns_mam..":prefs", function(event) - end - end - return true; -+end -+ -+module:hook("iq/self/"..xmlns_mam1..":prefs", function(event) -+ return handle_prefs(event, xmlns_mam1) -+end); -+ -+module:hook("iq/self/"..xmlns_mam0..":prefs", function(event) -+ return handle_prefs(event, xmlns_mam0) - end); - --local query_form = dataform { -- { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; }; -+local query_form = {} -+query_form[xmlns_mam1] = dataform { -+ { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam1; }; -+ { name = "with"; type = "jid-single"; }; -+ { name = "start"; type = "text-single" }; -+ { name = "end"; type = "text-single"; }; -+}; -+query_form[xmlns_mam0] = dataform { -+ { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam0; }; - { name = "with"; type = "jid-single"; }; - { name = "start"; type = "text-single" }; - { name = "end"; type = "text-single"; }; - }; - - -- Serve form --module:hook("iq-get/self/"..xmlns_mam..":query", function(event) -+local function get_form(event, xmlns_mam) - local origin, stanza = event.origin, event.stanza; -- origin.send(st.reply(stanza):add_child(query_form:form())); -+ origin.send(st.reply(stanza):add_child(query_form[xmlns_mam]:form())); - return true; -+end -+ -+module:hook("iq-get/self/"..xmlns_mam1..":query", function(event) -+ return get_form(event, xmlns_mam1); -+end); -+ -+module:hook("iq-get/self/"..xmlns_mam0..":query", function(event) -+ return get_form(event, xmlns_mam0); - end); - - -- Handle archive queries --module:hook("iq-set/self/"..xmlns_mam..":query", function(event) -+local function handle_query(event, xmlns_mam) - local origin, stanza = event.origin, event.stanza; - local query = stanza.tags[1]; - local qid = query.attr.queryid; -@@ -101,7 +125,7 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event) - local form = query:get_child("x", "jabber:x:data"); - if form then - local err; -- form, err = query_form:data(form); -+ form, err = query_form[xmlns_mam]:data(form); - if err then - origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); - return true; -@@ -145,9 +169,13 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event) - return true; - end - local total = tonumber(err); -+ local msg_reply_attr; -+ -+ if xmlns_mam == xmlns_mam0 then -+ origin.send(st.reply(stanza)); -+ msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to }; -+ end - -- origin.send(st.reply(stanza)); -- local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to }; - - local results = {}; - -@@ -192,11 +220,27 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event) - -- That's all folks! - module:log("debug", "Archive query %s completed", tostring(qid)); - -- origin.send(st.message(msg_reply_attr) -- :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete }) -- :add_child(rsm.generate { -- first = first, last = last, count = total })); -+ local reply_stanza; -+ if xmlns_mam == xmlns_mam0 then -+ reply_stanza = st.message(msg_reply_attr); -+ else -+ reply_stanza = st.reply(stanza); -+ end -+ -+ if form or xmlns_mam == xmlns_mam0 then -+ reply_stanza:tag("fin",{ xmlns = xmlns_mam, queryid = qid, complete = complete }) -+ :add_child(rsm.generate { first = first, last = last, count = total }) -+ end -+ origin.send(reply_stanza); - return true; -+end -+ -+module:hook("iq-set/self/"..xmlns_mam1..":query", function(event) -+ return handle_query(event, xmlns_mam1); -+end); -+ -+module:hook("iq-set/self/"..xmlns_mam0..":query", function(event) -+ return handle_query(event, xmlns_mam0); - end); - - local function has_in_roster(user, who) -@@ -330,9 +374,11 @@ module:hook("pre-message/full", c2s_message_handler, 2); - module:hook("message/bare", message_handler, 2); - module:hook("message/full", message_handler, 2); - --module:add_feature(xmlns_mam); -- COMPAT with XEP-0313 v 0.1 -+module:add_feature(xmlns_mam0); -- COMPAT with XEP-0313 v 0.1 -+module:add_feature(xmlns_mam1); -- COMPAT with XEP-0313 v 0.5 - - module:hook("account-disco-info", function(event) -- (event.reply or event.stanza):tag("feature", {var=xmlns_mam}):up(); -+ (event.reply or event.stanza):tag("feature", {var=xmlns_mam0}):up(); -+ (event.reply or event.stanza):tag("feature", {var=xmlns_mam1}):up(); - end); - --- -2.11.0 - diff --git a/debian/patches/0002-TT-8297-mam-implement-archive-to-DB.patch b/debian/patches/0002-TT-8297-mam-implement-archive-to-DB.patch index 40cce13..59051ba 100644 --- a/debian/patches/0002-TT-8297-mam-implement-archive-to-DB.patch +++ b/debian/patches/0002-TT-8297-mam-implement-archive-to-DB.patch @@ -1,27 +1,28 @@ -From 754fb5b63c5d3b4125ec80b8ecfdfdd3676242b3 Mon Sep 17 00:00:00 2001 +From 0dd1a7404c27b4a9352f196d52462b7ae6237590 Mon Sep 17 00:00:00 2001 From: Victor Seva Date: Tue, 3 Jan 2017 17:14:49 +0100 Subject: [PATCH] TT#8297 mam: implement archive to DB Change-Id: If9644ac66fdd257a430cfb7fa185230c77cd783e --- - plugins/mod_mam/mod_mam.lua | 16 +--- + plugins/mod_mam/mod_mam.lua | 17 +--- plugins/mod_mam/sipwise_archive.lib.lua | 165 ++++++++++++++++++++++++++++++++ - 2 files changed, 166 insertions(+), 15 deletions(-) + 2 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 plugins/mod_mam/sipwise_archive.lib.lua diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua -index dbdd92a..d7f4f61 100644 +index a4aa014..13e0a0b 100644 --- a/plugins/mod_mam/mod_mam.lua +++ b/plugins/mod_mam/mod_mam.lua -@@ -37,21 +37,7 @@ if global_default_policy ~= "roster" then +@@ -40,22 +40,7 @@ if global_default_policy ~= "roster" then global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy); end --local archive_store = "archive2"; --local archive = assert(module:open_store(archive_store, "archive")); +-local archive_store = module:get_option_string("archive_store", "archive2"); +-local archive = module:open_store(archive_store, "archive"); - -if archive.name == "null" or not archive.find then +- -- luacheck: ignore 631 - if not archive.find then - module:log("debug", "Attempt to open archive storage returned a valid driver but it does not seem to implement the storage API"); - module:log("debug", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or ""); diff --git a/debian/patches/0005-TT-9317-mod_mam-don-t-store-bodyless-chat-messages.patch b/debian/patches/0005-TT-9317-mod_mam-don-t-store-bodyless-chat-messages.patch index ff5d434..7e997bc 100644 --- a/debian/patches/0005-TT-9317-mod_mam-don-t-store-bodyless-chat-messages.patch +++ b/debian/patches/0005-TT-9317-mod_mam-don-t-store-bodyless-chat-messages.patch @@ -1,33 +1,38 @@ -From 7fc4d61ef370de25c69a0a637c0d0e2624e61dc0 Mon Sep 17 00:00:00 2001 +From 9e01a7a10fc1cd8a2d7dc6fe65bdccccf90f8668 Mon Sep 17 00:00:00 2001 From: Victor Seva Date: Thu, 19 Jan 2017 17:13:12 +0100 Subject: [PATCH] TT#9317 mod_mam: don't store bodyless chat messages -* implement store hints https://prosody.im/issues/issue/750 +* implement store hints + https://prosody.im/issues/issue/750 + mod_mam doesn't honor forced storage hint. + When a message without a body is received mod_mam always ignores the forced + storage hint, even if is present. + This can cause issue with OMEMO, which sends message elements without a body Change-Id: I590b1fb9bd95afdce6a117778052d11d8102f718 --- - plugins/mod_mam/mod_mam.lua | 24 +++++++++++++++++++----- - 1 file changed, 19 insertions(+), 5 deletions(-) + plugins/mod_mam/mod_mam.lua | 23 +++++++++++++++++------ + 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua -index d7f4f61..bb604ed 100644 +index 13e0a0b..8d8ef62 100644 --- a/plugins/mod_mam/mod_mam.lua +++ b/plugins/mod_mam/mod_mam.lua -@@ -267,11 +267,25 @@ local function message_handler(event, c2s) - local orig_from = stanza.attr.from; - local orig_to = stanza.attr.to or orig_from; - -- Stanza without 'to' are treated as if it was to their own bare jid -- +@@ -269,19 +269,30 @@ local function message_handler(event, c2s) + return tag; + end); + - -- We store chat messages or normal messages that have a body - if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then - log("debug", "Not archiving stanza: %s (type)", stanza:top_tag()); - return; -+ local body = stanza:get_child("body"); -+ local force_store = stanza:get_child("store", "urn:xmpp:hints"); -+ -+ if not force_store then +- end +- + -- or if hints suggest we shouldn't + if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store + -- We store chat messages or normal messages that have a body ++ local body = stanza:get_child("body"); + if not(orig_type == "chat" or (orig_type == "normal" and body)) then + log("debug", "Not archiving stanza: %s (type)", stanza:top_tag()); + return; @@ -36,6 +41,12 @@ index d7f4f61..bb604ed 100644 + stanza:top_tag()); + return; + end ++ + if stanza:get_child("no-permanent-store", "urn:xmpp:hints") + or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store + log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag()); + return; + end + else + log("debug", "store hint detected"); + if orig_type == 'error' then @@ -43,8 +54,8 @@ index d7f4f61..bb604ed 100644 + return; + end end - -- or if hints suggest we shouldn't - if stanza:get_child("no-permanent-storage", "urn:xmpp:hints") -- The XEP needs to decide on "store" or "storage" + + -- Check with the users preferences -- 2.11.0 diff --git a/debian/patches/series b/debian/patches/series index e2fd25f..1759217 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1,5 +1,4 @@ mod_auth_sql.path -0001-TT-7482-mod_man-support-mam-version-0.5-urn-xmpp-mam.patch 0002-TT-8297-mam-implement-archive-to-DB.patch 0003-TT-9303-mod_mam-sipwise_archive.lib-fix-crash-at-fin.patch 0004-TT-9311-mod_mam-sipwise_archive.lib-full-username.patch diff --git a/plugins/mod_mam/README.markdown b/plugins/mod_mam/README.markdown index 38a0591..bba0708 100644 --- a/plugins/mod_mam/README.markdown +++ b/plugins/mod_mam/README.markdown @@ -69,7 +69,7 @@ Query size limits This is the largest number of messages that are allowed to be retrieved in one request *page*. A query that does not fit in one page will include a reference to the next page, letting clients page through the -result set. Setting large number is not recomended, as Prosody will be +result set. Setting large number is not recommended, as Prosody will be blocked while processing the request and will not be able to do anything else. @@ -117,12 +117,13 @@ default_archive_policy = true Compatibility ============= - ------- --------------- - trunk Works - 0.10 Works + ------- ----------------------- + trunk Included with Prosody + 0.10 Included with Prosody 0.9 Works 0.8 Does not work - ------- --------------- + ------- ----------------------- + [^1]: Might be changed to "mam" at some point diff --git a/plugins/mod_mam/fallback_archive.lib.lua b/plugins/mod_mam/fallback_archive.lib.lua index 33441b8..96c72b0 100644 --- a/plugins/mod_mam/fallback_archive.lib.lua +++ b/plugins/mod_mam/fallback_archive.lib.lua @@ -65,7 +65,6 @@ function archive_store:delete(username, query) local archive = store[username]; if not archive then return true; end -- no messages, nothing to delete - local start, stop, step = 1, archive[0] or #archive, 1; local qstart = query.start or -math.huge; local qend = query["end"] or math.huge; local qwith = query.with; diff --git a/plugins/mod_mam/mamprefs.lib.lua b/plugins/mod_mam/mamprefs.lib.lua index 98e74bd..9f2b8ae 100644 --- a/plugins/mod_mam/mamprefs.lib.lua +++ b/plugins/mod_mam/mamprefs.lib.lua @@ -2,10 +2,12 @@ -- Copyright (C) 2011-2013 Kim Alvefur -- -- This file is MIT/X11 licensed. +-- luacheck: ignore 122/prosody local global_default_policy = module:get_option("default_archive_policy", true); do + -- luacheck: ignore 211/prefs_format local prefs_format = { [false] = "roster", -- default ::= true | false | "roster" @@ -15,8 +17,9 @@ do }; end -local sessions = hosts[module.host].sessions; -local prefs = module:open_store("archive2_prefs"); +local sessions = prosody.hosts[module.host].sessions; +local archive_store = module:get_option_string("archive_store", "archive"); +local prefs = module:open_store(archive_store .. "_prefs"); local function get_prefs(user) local user_sessions = sessions[user]; diff --git a/plugins/mod_mam/mamprefsxml.lib.lua b/plugins/mod_mam/mamprefsxml.lib.lua index a0d4624..4b3a50b 100644 --- a/plugins/mod_mam/mamprefsxml.lib.lua +++ b/plugins/mod_mam/mamprefsxml.lib.lua @@ -4,7 +4,6 @@ -- This file is MIT/X11 licensed. local st = require"util.stanza"; -local xmlns_mam = "urn:xmpp:mam:0"; local default_attrs = { always = true, [true] = "always", @@ -12,7 +11,7 @@ local default_attrs = { roster = "roster", } -local function tostanza(prefs) +local function tostanza(prefs, xmlns_mam) local default = prefs[false]; default = default_attrs[default]; local prefstanza = st.stanza("prefs", { xmlns = xmlns_mam, default = default }); diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua index fe69e33..a4aa014 100644 --- a/plugins/mod_mam/mod_mam.lua +++ b/plugins/mod_mam/mod_mam.lua @@ -3,9 +3,12 @@ -- -- This file is MIT/X11 licensed. -local xmlns_mam = "urn:xmpp:mam:0"; +local xmlns_mam0 = "urn:xmpp:mam:0"; +local xmlns_mam1 = "urn:xmpp:mam:1"; +local xmlns_mam2 = "urn:xmpp:mam:2"; local xmlns_delay = "urn:xmpp:delay"; local xmlns_forward = "urn:xmpp:forward:0"; +local xmlns_st_id = "urn:xmpp:sid:0"; local um = require "core.usermanager"; local st = require "util.stanza"; @@ -16,6 +19,7 @@ local prefs_to_stanza = module:require"mamprefsxml".tostanza; local prefs_from_stanza = module:require"mamprefsxml".fromstanza; local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; +local jid_prepped_split = require "util.jid".prepped_split; local dataform = require "util.dataforms".new; local host = module.host; @@ -36,10 +40,11 @@ if global_default_policy ~= "roster" then global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy); end -local archive_store = "archive2"; -local archive = assert(module:open_store(archive_store, "archive")); +local archive_store = module:get_option_string("archive_store", "archive2"); +local archive = module:open_store(archive_store, "archive"); if archive.name == "null" or not archive.find then + -- luacheck: ignore 631 if not archive.find then module:log("debug", "Attempt to open archive storage returned a valid driver but it does not seem to implement the storage API"); module:log("debug", "mod_%s does not support archiving", archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or ""); @@ -54,11 +59,12 @@ end local cleanup; -- Handle prefs. -module:hook("iq/self/"..xmlns_mam..":prefs", function(event) +local function handle_prefs(event) local origin, stanza = event.origin, event.stanza; + local xmlns_mam = stanza.tags[1].attr.xmlns; local user = origin.username; if stanza.attr.type == "get" then - local prefs = prefs_to_stanza(get_prefs(user)); + local prefs = prefs_to_stanza(get_prefs(user), xmlns_mam); local reply = st.reply(stanza):add_child(prefs); origin.send(reply); else -- type == "set" @@ -72,25 +78,36 @@ module:hook("iq/self/"..xmlns_mam..":prefs", function(event) end end return true; -end); +end + +module:hook("iq/self/"..xmlns_mam0..":prefs", handle_prefs); +module:hook("iq/self/"..xmlns_mam1..":prefs", handle_prefs); +module:hook("iq/self/"..xmlns_mam2..":prefs", handle_prefs); local query_form = dataform { - { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; }; + { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam0; }; { name = "with"; type = "jid-single"; }; { name = "start"; type = "text-single" }; { name = "end"; type = "text-single"; }; }; -- Serve form -module:hook("iq-get/self/"..xmlns_mam..":query", function(event) +local function handle_get_form(event) local origin, stanza = event.origin, event.stanza; - origin.send(st.reply(stanza):add_child(query_form:form())); + local xmlns_mam = stanza.tags[1].attr.xmlns; + query_form[1].value = xmlns_mam; + origin.send(st.reply(stanza):query(xmlns_mam):add_child(query_form:form())); return true; -end); +end + +module:hook("iq-get/self/"..xmlns_mam0..":query", handle_get_form); +module:hook("iq-get/self/"..xmlns_mam1..":query", handle_get_form); +module:hook("iq-get/self/"..xmlns_mam2..":query", handle_get_form); -- Handle archive queries -module:hook("iq-set/self/"..xmlns_mam..":query", function(event) +local function handle_mam_query(event) local origin, stanza = event.origin, event.stanza; + local xmlns_mam = stanza.tags[1].attr.xmlns; local query = stanza.tags[1]; local qid = query.attr.queryid; @@ -101,6 +118,7 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event) local form = query:get_child("x", "jabber:x:data"); if form then local err; + query_form[1].value = xmlns_mam; form, err = query_form:data(form); if err then origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); @@ -129,7 +147,6 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event) local before, after = qset and qset.before, qset and qset.after; if type(before) ~= "string" then before = nil; end - -- Load all the data! local data, err = archive:find(origin.username, { start = qstart; ["end"] = qend; -- Time range @@ -146,7 +163,9 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event) end local total = tonumber(err); - origin.send(st.reply(stanza)); + if xmlns_mam == xmlns_mam0 then + origin.send(st.reply(stanza)); + end local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to }; local results = {}; @@ -192,12 +211,23 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event) -- That's all folks! module:log("debug", "Archive query %s completed", tostring(qid)); - origin.send(st.message(msg_reply_attr) - :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete }) + local fin; + if xmlns_mam == xmlns_mam0 then + fin = st.message(msg_reply_attr); + else + fin = st.reply(stanza); + end + do + fin:tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete }) :add_child(rsm.generate { - first = first, last = last, count = total })); + first = first, last = last, count = total }) + end + origin.send(fin); return true; -end); +end +module:hook("iq-set/self/"..xmlns_mam0..":query", handle_mam_query); +module:hook("iq-set/self/"..xmlns_mam1..":query", handle_mam_query); +module:hook("iq-set/self/"..xmlns_mam2..":query", handle_mam_query); local function has_in_roster(user, who) local roster = rm_load_roster(user, host); @@ -238,32 +268,46 @@ local function message_handler(event, c2s) local orig_to = stanza.attr.to or orig_from; -- Stanza without 'to' are treated as if it was to their own bare jid + -- Whos storage do we put it in? + local store_user = c2s and origin.username or jid_split(orig_to); + -- And who are they chatting with? + local with = jid_bare(c2s and orig_to or orig_from); + + -- Filter out that claim to be from us + stanza:maptags(function (tag) + if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id then + local by_user, by_host, res = jid_prepped_split(tag.attr.by); + if not res and by_host == module.host and by_user == store_user then + return nil; + end + end + return tag; + end); + -- We store chat messages or normal messages that have a body if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then log("debug", "Not archiving stanza: %s (type)", stanza:top_tag()); return; end + -- or if hints suggest we shouldn't - if 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 - log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag()); - return; + if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store + if stanza:get_child("no-permanent-store", "urn:xmpp:hints") + or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store + log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag()); + return; + end end - -- Whos storage do we put it in? - local store_user = c2s and origin.username or jid_split(orig_to); - -- And who are they chatting with? - local with = jid_bare(c2s and orig_to or orig_from); - -- Check with the users preferences if shall_store(store_user, with) then log("debug", "Archiving stanza: %s", stanza:top_tag()); -- And stash it - local ok, id = archive:append(store_user, nil, stanza, time_now(), with); + local ok = archive:append(store_user, nil, stanza, time_now(), with); if ok then + local id = ok; + stanza:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up(); if cleanup then cleanup[store_user] = true; end module:fire_event("archive-message-added", { origin = origin, stanza = stanza, for_user = store_user, id = id }); end @@ -276,6 +320,18 @@ local function c2s_message_handler(event) return message_handler(event, true); end +local function strip_stanza_id(event) + local strip_by = jid_bare(event.origin.full_jid); + event.stanza:maptags(function(tag) + if not ( tag.attr.xmlns == xmlns_st_id and tag.attr.by == strip_by ) then + return tag; + end + end); +end + +module:hook("pre-message/bare", strip_stanza_id, 0.01); +module:hook("pre-message/full", strip_stanza_id, 0.01); + local cleanup_after = module:get_option_string("archive_expires_after", "1w"); local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60); if cleanup_after ~= "never" then @@ -296,14 +352,20 @@ if cleanup_after ~= "never" then return false; end + -- Set of known users to do message expiry for + -- Populated either below or when new messages are added cleanup = {}; + -- Iterating over users is not supported by all authentication modules + -- Catch and ignore error if not supported pcall(function () + -- If this works, then we schedule cleanup for all known users on startup for user in um.users(module.host) do cleanup[user] = true; end end); + -- At odd intervals, delete old messages for one user module:add_timer(math.random(10, 60), function() local user = next(cleanup); if user then @@ -330,9 +392,12 @@ module:hook("pre-message/full", c2s_message_handler, 2); module:hook("message/bare", message_handler, 2); module:hook("message/full", message_handler, 2); -module:add_feature(xmlns_mam); -- COMPAT with XEP-0313 v 0.1 +module:add_feature(xmlns_mam0); -- COMPAT with XEP-0313 v 0.1 module:hook("account-disco-info", function(event) - (event.reply or event.stanza):tag("feature", {var=xmlns_mam}):up(); + (event.reply or event.stanza):tag("feature", {var=xmlns_mam0}):up(); + (event.reply or event.stanza):tag("feature", {var=xmlns_mam1}):up(); + (event.reply or event.stanza):tag("feature", {var=xmlns_mam2}):up(); + (event.reply or event.stanza):tag("feature", {var=xmlns_st_id}):up(); end); diff --git a/plugins/mod_smacks/README.markdown b/plugins/mod_smacks/README.markdown index ea8c9fe..f5e81d4 100644 --- a/plugins/mod_smacks/README.markdown +++ b/plugins/mod_smacks/README.markdown @@ -39,36 +39,45 @@ These events are: "smacks-ack-delayed", "smacks-hibernation-start" and "smacks-hibernation-end". See [mod_cloud_notify] for details on how this events are used there. +Use prosody 0.10+ to have per user limits on allowed sessions in hibernation +state and allowed sessions for which the h-value is kept even after the +hibernation timed out. +These are settable using `smacks_max_hibernated_sessions` and `smacks_max_old_sessions`. + Configuration ============= - Option Default Description - ------------------------------ ----------------- ----------------------------------------------------------------------------------------- - `smacks_hibernation_time` 300 (5 minutes) The number of seconds a disconnected session should stay alive for (to allow reconnect) - `smacks_enabled_s2s` false Enable Stream Management on server connections? *Experimental* - `smacks_max_unacked_stanzas` 0 How many stanzas to send before requesting acknowledgement - `smacks_max_ack_delay` 60 (1 minute) The number of seconds an ack must be unanswered to trigger an "smacks-ack-delayed" event + Option Default Description + ---------------------------------- ----------------- ------------------------------------------------------------------------------------------------------------------ + `smacks_hibernation_time` 300 (5 minutes) The number of seconds a disconnected session should stay alive for (to allow reconnect) + `smacks_enabled_s2s` false Enable Stream Management on server connections? *Experimental* + `smacks_max_unacked_stanzas` 0 How many stanzas to send before requesting acknowledgement + `smacks_max_ack_delay` 60 (1 minute) The number of seconds an ack must be unanswered to trigger an "smacks-ack-delayed" event + `smacks_max_hibernated_sessions` 10 The number of allowed sessions in hibernated state (limited per user) + `smacks_max_old_sessions` 10 The number of allowed sessions with timed out hibernation for which the h-value is still kept (limited per user) Compatibility ============= - ----- ----------------------------------- + ----- ----------------------------------------------------------------------------- 0.10 Works - 0.9 Works - 0.8 Works, use version [7693724881b3] - ----- ----------------------------------- + 0.9 Works, no per user limit of hibernated sessions + 0.8 Works, no per user limit of hibernated sessions, use version [7693724881b3] + ----- ----------------------------------------------------------------------------- Clients ======= -Clients that support XEP-0198: +Clients that support [XEP-0198]: -- Gajim +- Gajim (Linux, Windows, OS X) +- Conversations (Android) +- ChatSecure (iOS) - Swift (but not resumption, as of version 2.0 and alphas of 3.0) - Psi (in an unreleased branch) -- Conversations -- Yaxim +- Yaxim (Android) +- Monal (iOS) [7693724881b3]: //hg.prosody.im/prosody-modules/raw-file/7693724881b3/mod_smacks/mod_smacks.lua [mod_smacks_offline]: //modules.prosody.im/mod_smacks_offline diff --git a/plugins/mod_smacks/mod_smacks.lua b/plugins/mod_smacks/mod_smacks.lua index e32f19f..d37a60c 100644 --- a/plugins/mod_smacks/mod_smacks.lua +++ b/plugins/mod_smacks/mod_smacks.lua @@ -5,13 +5,15 @@ -- Copyright (C) 2012-2015 Kim Alvefur -- Copyright (C) 2012 Thijs Alkemade -- Copyright (C) 2014 Florian Zeitz --- Copyright (C) 2016 Thilo Molitor +-- Copyright (C) 2016-2017 Thilo Molitor -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local st = require "util.stanza"; +local dep = require "util.dependencies"; +local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+ local uuid_generate = require "util.uuid".generate; local t_insert, t_remove = table.insert, table.remove; @@ -35,18 +37,74 @@ local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false); local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false); local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0); local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 60); +local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10); +local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10); local core_process_stanza = prosody.core_process_stanza; local sessionmanager = require"core.sessionmanager"; local c2s_sessions = module:shared("/*/c2s/sessions"); -local session_registry = {}; + +local function init_session_cache(max_entries, evict_callback) + -- old prosody version < 0.10 (no limiting at all!) + if not cache then + local store = {}; + return { + get = function(user, key) return store[key]; end; + set = function(user, key, value) store[key] = value; end; + }; + end + + -- use per user limited cache for prosody >= 0.10 + local stores = {}; + return { + get = function(user, key) + if not stores[user] then + stores[user] = cache.new(max_entries, evict_callback); + end + return stores[user]:get(key); + end; + set = function(user, key, value) + if not stores[user] then stores[user] = cache.new(max_entries, evict_callback); end + stores[user]:set(key, value); + -- remove empty caches completely + if not stores[user]:count() then stores[user] = nil; end + end; + }; +end +local old_session_registry = init_session_cache(max_old_sessions, nil); +local session_registry = init_session_cache(max_hibernated_sessions, function(resumption_token, session) + if session.destroyed then return; end + session.log("warn", "User has too much hibernated sessions, removing oldest session (token: %s)", resumption_token); + -- store old session's h values on force delete + -- save only actual h value and username/host (for security) + old_session_registry.set(session.username, resumption_token, { + h = session.handled_stanza_count, + username = session.username, + host = session.host + }); + return true; -- allow session to be removed from full cache to make room for new one +end); + +local function stoppable_timer(delay, callback) + local stopped = false; + return { + stop = function () stopped = true end; + module:add_timer(delay, function (t) + if stopped then return; end + return callback(t); + end); + }; +end local function delayed_ack_function(session) - -- fire event only when configured to do so - if delayed_ack_timeout > 0 and session.awaiting_ack and not session.outgoing_stanza_queue == nil then - session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d", #session.outgoing_stanza_queue); + -- fire event only if configured to do so and our session is not hibernated or destroyed + if delayed_ack_timeout > 0 and session.awaiting_ack + and not session.hibernating and not session.destroyed then + session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d", + session.outgoing_stanza_queue and #session.outgoing_stanza_queue or 0); module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue}); end + session.delayed_ack_timer = nil; end local function can_do_smacks(session, advertise_only) @@ -80,6 +138,38 @@ module:hook("s2s-stream-features", end end); +local function request_ack_if_needed(session, force) + local queue = session.outgoing_stanza_queue; + if session.awaiting_ack == nil then + if (#queue > max_unacked_stanzas and session.last_queue_count ~= #queue) or force then + session.log("debug", "Queuing (in a moment)"); + session.awaiting_ack = false; + session.awaiting_ack_timer = stoppable_timer(1e-06, function () + if not session.awaiting_ack then + session.log("debug", "Sending (inside timer, before send)"); + (session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks })) + session.log("debug", "Sending (inside timer, after send)"); + session.awaiting_ack = true; + if not session.delayed_ack_timer then + session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function() + delayed_ack_function(session); + end); + end + end + end); + end + -- Trigger "smacks-ack-delayed"-event if we added new (ackable) stanzas to the outgoing queue + -- and there isn't already a timer for this event running. + -- If we wouldn't do this, stanzas added to the queue after the first "smacks-ack-delayed"-event + -- would not trigger this event (again). + if #queue > max_unacked_stanzas and session.awaiting_ack and session.delayed_ack_timer == nil then + session.log("debug", "Calling delayed_ack_function directly (still waiting for ack)"); + delayed_ack_function(session); + end + end + session.last_queue_count = #queue; +end + local function outgoing_stanza_filter(stanza, session) 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 @@ -97,21 +187,7 @@ local function outgoing_stanza_filter(stanza, session) session.log("debug", "hibernating, stanza queued"); return nil; end - if #queue > max_unacked_stanzas and session.awaiting_ack == nil then - session.log("debug", "Queuing (in a moment)"); - session.awaiting_ack = false; - session.awaiting_ack_timer = module:add_timer(1e-06, function () - if not session.awaiting_ack then - session.log("debug", "Sending (before send)"); - (session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks })) - session.log("debug", "Sending (after send)"); - session.awaiting_ack = true; - session.delayed_ack_timer = module:add_timer(delayed_ack_timeout, function() - delayed_ack_function(session); - end); - end - end); - end + request_ack_if_needed(session, false); end return stanza; end @@ -135,9 +211,14 @@ local function wrap_session_out(session, resume) local session_close = session.close; function session.close(...) if session.resumption_token then - session_registry[session.resumption_token] = nil; + session_registry.set(session.username, session.resumption_token, nil); + old_session_registry.set(session.username, session.resumption_token, nil); session.resumption_token = nil; end + -- send out last ack as per revision 1.5.2 of XEP-0198 + if session.smacks and session.conn then + (session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks, h = tostring(session.handled_stanza_count) })); + end return session_close(...); end return session; @@ -175,7 +256,7 @@ function handle_enable(session, stanza, xmlns_sm) local resume = stanza.attr.resume; if resume == "true" or resume == "1" then resume_token = uuid_generate(); - session_registry[resume_token] = session; + session_registry.set(session.username, resume_token, session); session.resumption_token = resume_token; end (session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume })); @@ -186,7 +267,7 @@ module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) - module:add_timer(1e-6, function () + stoppable_timer(1e-6, function () if can_do_smacks(session) then if stanza:get_child("sm", xmlns_sm3) then session.sends2s(st.stanza("enable", sm3_attr)); @@ -236,9 +317,10 @@ function handle_a(origin, stanza) end if origin.delayed_ack_timer then origin.delayed_ack_timer:stop(); + origin.delayed_ack_timer = nil; end -- Remove handled stanzas from outgoing_stanza_queue - --log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or ""); + -- origin.log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or ""); local h = tonumber(stanza.attr.h); if not h then origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; }; @@ -258,6 +340,7 @@ function handle_a(origin, stanza) end origin.log("debug", "#queue = %d", #queue); origin.last_acknowledged_stanza = origin.last_acknowledged_stanza + handled_stanza_count; + request_ack_if_needed(origin, false) return true; end module:hook_stanza(xmlns_sm2, "a", handle_a); @@ -314,7 +397,13 @@ module:hook("pre-resource-unbind", function (event) -- otherwise the session resumed and re-hibernated. and session.hibernating == hibernate_time then session.log("debug", "Destroying session for hibernating too long"); - session_registry[session.resumption_token] = nil; + session_registry.set(session.username, session.resumption_token, nil); + -- save only actual h value and username/host (for security) + old_session_registry.set(session.username, session.resumption_token, { + h = session.handled_stanza_count, + username = session.username, + host = session.host + }); session.resumption_token = nil; sessionmanager.destroy_session(session); else @@ -323,7 +412,6 @@ module:hook("pre-resource-unbind", function (event) end); return true; -- Postpone destruction for now end - end end); @@ -356,12 +444,21 @@ function handle_resume(session, stanza, xmlns_sm) end local id = stanza.attr.previd; - local original_session = session_registry[id]; + local original_session = session_registry.get(session.username, id); if not original_session then session.log("debug", "Tried to resume non-existent session with id %s", id); - session.send(st.stanza("failed", { xmlns = xmlns_sm }) - :tag("item-not-found", { xmlns = xmlns_errors }) - ); + local old_session = old_session_registry.get(session.username, id); + if old_session and session.username == old_session.username + and session.host == old_session.host + and old_session.h then + session.send(st.stanza("failed", { xmlns = xmlns_sm, h = tostring(old_session.h) }) + :tag("item-not-found", { xmlns = xmlns_errors }) + ); + else + session.send(st.stanza("failed", { xmlns = xmlns_sm }) + :tag("item-not-found", { xmlns = xmlns_errors }) + ); + end; elseif session.username == original_session.username and session.host == original_session.host then session.log("debug", "mod_smacks resuming existing session..."); @@ -375,6 +472,7 @@ function handle_resume(session, stanza, xmlns_sm) original_session.ip = session.ip; original_session.conn = session.conn; original_session.send = session.send; + original_session.close = session.close; original_session.filter = session.filter; original_session.filter.session = original_session; original_session.filters = session.filters; @@ -387,7 +485,7 @@ function handle_resume(session, stanza, xmlns_sm) -- Similar for connlisteners c2s_sessions[session.conn] = original_session; - session.send(st.stanza("resumed", { xmlns = xmlns_sm, + original_session.send(st.stanza("resumed", { xmlns = xmlns_sm, h = original_session.handled_stanza_count, previd = id })); -- Fake an with the h of the from the client @@ -397,12 +495,17 @@ function handle_resume(session, stanza, xmlns_sm) -- Ok, we need to re-send any stanzas that the client didn't see -- ...they are what is now left in the outgoing stanza queue local queue = original_session.outgoing_stanza_queue; - module:fire_event("smacks-hibernation-end", {origin = session, queue = queue}); - session.log("debug", "#queue = %d", #queue); + module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue}); + original_session.log("debug", "#queue = %d", #queue); for i=1,#queue do - session.send(queue[i]); + original_session.send(queue[i]); end - session.log("debug", "#queue = %d -- after send", #queue); + original_session.log("debug", "#queue = %d -- after send", #queue); + function session.send(stanza) + session.log("warn", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza)); + return false; + end + request_ack_if_needed(original_session, true); else module:log("warn", "Client %s@%s[%s] tried to resume stream for %s@%s[%s]", session.username or "?", session.host or "?", session.type, @@ -424,6 +527,7 @@ local function handle_read_timeout(event) end if session.delayed_ack_timer then session.delayed_ack_timer:stop(); + session.delayed_ack_timer = nil; end return false; -- Kick the session end @@ -431,9 +535,11 @@ local function handle_read_timeout(event) session.awaiting_ack = false; (session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks })); session.awaiting_ack = true; - session.delayed_ack_timer = module:add_timer(delayed_ack_timeout, function() - delayed_ack_function(session); - end); + if not session.delayed_ack_timer then + session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function() + delayed_ack_function(session); + end); + end return true; end end diff --git a/prosody-modules.revision b/prosody-modules.revision index 56be96e..4a9a6a3 100644 --- a/prosody-modules.revision +++ b/prosody-modules.revision @@ -1 +1 @@ -51cf82d36a8a +2e1a4740adee