diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/changelog prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/changelog --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/changelog 2019-11-02 18:59:38.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/changelog 2020-01-28 10:09:01.000000000 +0000 @@ -1,3 +1,11 @@ +prosody-modules (0.0~hg20200128.09e7e880e056+dfsg-1) unstable; urgency=medium + + * New upstream version 0.0~hg20200128.09e7e880e056+dfsg + * add mod_net_dovecotauth (Closes: #923384) + * update upstream fixes + + -- Victor Seva Tue, 28 Jan 2020 11:09:01 +0100 + prosody-modules (0.0~hg20191101.19e43b7a969d+dfsg-1) unstable; urgency=medium * new upstream snapshot diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/control prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/control --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/control 2019-11-02 18:59:38.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/control 2020-01-28 10:09:01.000000000 +0000 @@ -20,7 +20,7 @@ prosody (>= 0.11.2~), ${misc:Depends}, Recommends: - lua-ldap + lua-ldap, Multi-Arch: foreign Description: Selection of community modules for Prosody This package contains extensions to the Prosody XMPP server. @@ -66,6 +66,9 @@ to Prosody’s data store - mod_muc_log_http: provides a built-in web interface to view chatroom logs stored by mod_muc_log + - mod_net_dovecotauth: server implementation of the Dovecot + authentication protocol. It allows you to authenticate e.g. + Postfix against your Prosody installation. - mod_onions: allow federation (s2s) to Tor hidden services - mod_pastebin: redirect long messages to built-in pastebin - mod_privacy_lists: XEP-0016: privacy lists diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/install prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/install --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/install 2019-11-02 18:56:42.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/install 2020-01-28 10:09:01.000000000 +0000 @@ -32,6 +32,7 @@ mod_mam_adhoc /usr/lib/prosody/modules/ mod_muc_log /usr/lib/prosody/modules/ mod_muc_log_http /usr/lib/prosody/modules/ +mod_net_dovecotauth /usr/lib/prosody/modules/ mod_onions /usr/lib/prosody/modules/ mod_pastebin /usr/lib/prosody/modules/ mod_privacy_lists /usr/lib/prosody/modules/ diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/patches/0003-ldap-improve-checks.patch prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/patches/0003-ldap-improve-checks.patch --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/patches/0003-ldap-improve-checks.patch 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/patches/0003-ldap-improve-checks.patch 2020-01-28 10:09:01.000000000 +0000 @@ -0,0 +1,48 @@ +From: Victor Seva +Date: Tue, 28 Jan 2020 10:45:37 +0100 +Subject: ldap improve checks + +--- + mod_auth_ldap/mod_auth_ldap.lua | 5 ++++- + mod_auth_ldap2/mod_auth_ldap2.lua | 5 ++++- + 2 files changed, 8 insertions(+), 2 deletions(-) + +diff --git a/mod_auth_ldap/mod_auth_ldap.lua b/mod_auth_ldap/mod_auth_ldap.lua +index b5dea86..4d484aa 100644 +--- a/mod_auth_ldap/mod_auth_ldap.lua ++++ b/mod_auth_ldap/mod_auth_ldap.lua +@@ -135,7 +135,10 @@ end + + if ldap_admins then + function provider.is_admin(jid) +- local username = jid_split(jid); ++ local username, user_host = jid_split(jid); ++ if user_host ~= module.host then ++ return false; ++ end + return ldap_do("search", 2, { + base = ldap_base; + scope = ldap_scope; +diff --git a/mod_auth_ldap2/mod_auth_ldap2.lua b/mod_auth_ldap2/mod_auth_ldap2.lua +index c79849e..01b6300 100644 +--- a/mod_auth_ldap2/mod_auth_ldap2.lua ++++ b/mod_auth_ldap2/mod_auth_ldap2.lua +@@ -59,6 +59,10 @@ function provider.get_sasl_handler() + end + + function provider.is_admin(jid) ++ local username, userhost = jsplit(jid); ++ if userhost ~= module.host then ++ return false; ++ end + local admin_config = ldap.getparams().admin; + + if not admin_config then +@@ -66,7 +70,6 @@ function provider.is_admin(jid) + end + + local ld = ldap:getconnection(); +- local username = jsplit(jid); + local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username); + + return ldap.singlematch { diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/patches/series prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/patches/series --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/debian/patches/series 2019-11-02 18:56:42.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/debian/patches/series 2020-01-28 10:09:01.000000000 +0000 @@ -1,2 +1,3 @@ 0001-HA1b-simple-adaptation-to-meet-the-Debian-needs.patch 0002-mod_muc_log_http-remove-link-to-external-resource.patch +0003-ldap-improve-checks.patch diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/.luacheckrc prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/.luacheckrc --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/.luacheckrc 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/.luacheckrc 2020-01-28 09:32:42.000000000 +0000 @@ -1,6 +1,6 @@ cache = true allow_defined_top = true -unused_secondaries = false +--unused_secondaries = false max_line_length = 150 codes = true ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV" }; diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_auth_imap/auth_imap/sasl_imap.lib.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_auth_imap/auth_imap/sasl_imap.lib.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_auth_imap/auth_imap/sasl_imap.lib.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_auth_imap/auth_imap/sasl_imap.lib.lua 2020-01-28 09:32:42.000000000 +0000 @@ -85,7 +85,7 @@ log("debug", "imap greeting: '%s'", line); local caps = line:match("^%*%s+OK%s+(%b[])"); if not caps or not caps:match("^%[CAPABILITY ") then - conn:send("A CAPABILITY\n"); + conn:send("A CAPABILITY\r\n"); line = conn:receive("*l"); log("debug", "imap capabilities response: '%s'", line); caps = line:match("^%*%s+CAPABILITY%s+(.*)$"); @@ -158,7 +158,7 @@ self.selected = mechanism; local selectmsg = t_concat({ self.tag, "AUTHENTICATE", mechanism }, " "); log("debug", "Sending %d bytes: %q", #selectmsg, selectmsg); - local ok, err = self.conn:send(selectmsg.."\n"); + local ok, err = self.conn:send(selectmsg.."\r\n"); if not ok then log("error", "Could not write to socket: %s", err); return "failure", "internal-server-error", err @@ -181,7 +181,7 @@ message = message:gsub("^([^%z]*%z[^%z]+)(%z[^%z]+)$", "%1@"..self.realm.."%2"); end log("debug", "method:process(%d bytes): %q", #message, message:gsub("%z", ".")); - local ok, err = self.conn:send(b64(message).."\n"); + local ok, err = self.conn:send(b64(message).."\r\n"); if not ok then log("error", "Could not write to socket: %s", err); return "failure", "internal-server-error", err diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_bookmarks2/mod_bookmarks2.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_bookmarks2/mod_bookmarks2.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_bookmarks2/mod_bookmarks2.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_bookmarks2/mod_bookmarks2.lua 2020-01-28 09:32:42.000000000 +0000 @@ -9,9 +9,6 @@ local mod_pep = module:depends "pep"; local private_storage = module:open_store("private", "map"); -local legacy_ns = "storage:bookmarks"; -local ns = "urn:xmpp:bookmarks:0"; - local default_options = { ["persist_items"] = true; -- This should be much higher, the XEP recommends 10000 but mod_pep rejects that. @@ -48,15 +45,15 @@ module:log("debug", "Got no PEP bookmarks item for %s, returning empty private bookmarks", jid); session.send(st.reply(stanza):add_child(query)); else - module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, id); - session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to retrive bookmarks from PEP")); + module:log("error", "Failed to retrieve PEP bookmarks of %s: %s", jid, ret); + session.send(st.error_reply(stanza, "cancel", ret, "Failed to retrive bookmarks from PEP")); end return true; end local storage = st.stanza("storage", { xmlns = "storage:bookmarks" }); - for i in ipairs(ret) do - local item = ret[ret[i]]; + for _, item_id in ipairs(ret) do + local item = ret[item_id]; local conference = st.stanza("conference"); conference.attr.jid = item.attr.id; local bookmark = item:get_child("conference", "urn:xmpp:bookmarks:0"); @@ -95,13 +92,17 @@ a_password == b_password); end -local function publish_to_pep(jid, bookmarks) +local function publish_to_pep(jid, bookmarks, synchronise) local service = mod_pep.get_pep_service(jid_split(jid)); - -- If we set zero legacy bookmarks, purge the bookmarks 2 node. if #bookmarks.tags == 0 then - module:log("debug", "No bookmark in the set, purging instead."); - return service:purge("urn:xmpp:bookmarks:0", jid, true); + if synchronise then + -- If we set zero legacy bookmarks, purge the bookmarks 2 node. + module:log("debug", "No bookmark in the set, purging instead."); + return service:purge("urn:xmpp:bookmarks:0", jid, true); + else + return true; + end end -- Retrieve the current bookmarks2. @@ -168,12 +169,14 @@ end -- Now handle retracting items that have been removed. - for id in pairs(to_remove) do - module:log("debug", "Item %s removed from bookmarks.", id); - local ok, err = service:retract("urn:xmpp:bookmarks:0", jid, id, st.stanza("retract", { id = id })); - if not ok then - module:log("error", "Retracting item %s failed: %s", id, err); - return ok, err; + if synchronise then + for id in pairs(to_remove) do + module:log("debug", "Item %s removed from bookmarks.", id); + local ok, err = service:retract("urn:xmpp:bookmarks:0", jid, id, st.stanza("retract", { id = id })); + if not ok then + module:log("error", "Retracting item %s failed: %s", id, err); + return ok, err; + end end end return true; @@ -187,14 +190,14 @@ return; end - local bookmarks = query:get_child("storage", legacy_ns); + local bookmarks = query:get_child("storage", "storage:bookmarks"); if bookmarks == nil then return; end module:log("debug", "Private bookmarks set by client, publishing to pep."); - local ok, err = publish_to_pep(session.full_jid, bookmarks); + local ok, err = publish_to_pep(session.full_jid, bookmarks, true); if not ok then module:log("error", "Failed to publish to PEP bookmarks for %s@%s: %s", session.username, session.host, err); session.send(st.error_reply(stanza, "cancel", "internal-server-error", "Failed to store bookmarks to PEP")); @@ -211,6 +214,34 @@ local service = mod_pep.get_pep_service(username); local jid = username.."@"..session.host; + local ok, ret = service:get_items("storage:bookmarks", session.full_jid); + if ok then + module:log("debug", "Legacy PEP bookmarks found for %s, migrating.", jid); + local failed = false; + for _, item_id in ipairs(ret) do + local item = ret[item_id]; + if item.attr.id ~= "current" then + module:log("warn", "Legacy PEP bookmarks for %s isn’t using 'current' as its id: %s", jid, item.attr.id); + end + local bookmarks = item:get_child("storage", "storage:bookmarks"); + module:log("debug", "Got legacy PEP bookmarks of %s: %s", jid, bookmarks); + + local ok, err = publish_to_pep(session.full_jid, bookmarks, false); + if not ok then + module:log("error", "Failed to store legacy PEP bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err); + failed = true; + break; + end + end + if not failed then + module:log("debug", "Successfully migrated legacy PEP bookmarks of %s to bookmarks 2, attempting deletion of the node.", jid); + local ok, err = service:delete("storage:bookmarks", jid); + if not ok then + module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", jid, err); + end + end + end + local data, err = private_storage:get(username, "storage:storage:bookmarks"); if not data then module:log("debug", "No existing legacy bookmarks for %s, migration already done: %s", jid, err); @@ -224,24 +255,20 @@ local bookmarks = st.deserialize(data); module:log("debug", "Got legacy bookmarks of %s: %s", jid, bookmarks); - -- We don’t care if deleting succeeds or not, we only want to start with a non-existent node. - module:log("debug", "Deleting possibly existing PEP item for %s.", jid); - service:delete("urn:xmpp:bookmarks:0", jid); - - module:log("debug", "Going to store PEP item for %s.", jid); - local ok, err = publish_to_pep(session.full_jid, bookmarks); + module:log("debug", "Going to store legacy bookmarks to bookmarks 2 %s.", jid); + local ok, err = publish_to_pep(session.full_jid, bookmarks, false); if not ok then - module:log("error", "Failed to store bookmarks to PEP for %s, aborting migration: %s", jid, err); + module:log("error", "Failed to store legacy bookmarks to bookmarks 2 for %s, aborting migration: %s", jid, err); return; end - module:log("debug", "Stored bookmarks to PEP for %s.", jid); + module:log("debug", "Stored legacy bookmarks to bookmarks 2 for %s.", jid); local ok, err = private_storage:set(username, "storage:storage:bookmarks", nil); if not ok then - module:log("error", "Failed to remove private bookmarks of %s: %s", jid, err); + module:log("error", "Failed to remove legacy bookmarks of %s: %s", jid, err); return; end - module:log("debug", "Removed private bookmarks of %s, migration done!", jid); + module:log("debug", "Removed legacy bookmarks of %s, migration done!", jid); end local function on_node_created(event) @@ -249,26 +276,13 @@ if node ~= "storage:bookmarks" then return; end - local ok, node_config = service:get_node_config(node, actor); - if not ok then - module:log("error", "Failed to get node config of %s: %s", node, node_config); - return; - end - local changed = false; - for config_field, value in pairs(default_options) do - if node_config[config_field] ~= value then - node_config[config_field] = value; - changed = true; - end - end - if not changed then - return; - end - local ok, err = service:set_node_config(node, actor, node_config); + + module:log("debug", "Something tried to create legacy PEP bookmarks for %s.", actor); + local ok, err = service:delete("storage:bookmarks", actor); if not ok then - module:log("error", "Failed to set node config of %s: %s", node, err); - return; + module:log("error", "Failed to delete legacy PEP bookmarks for %s: %s", actor, err); end + module:log("debug", "Legacy PEP bookmarks node of %s deleted.", actor); end module:hook("iq/bare/jabber:iq:private:query", function (event) diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_bookmarks2/tests/bookmarks2.scs prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_bookmarks2/tests/bookmarks2.scs --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_bookmarks2/tests/bookmarks2.scs 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_bookmarks2/tests/bookmarks2.scs 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,180 @@ +# Pubsub: Bookmarks 2.0 + +[Client] Juliet + jid: admin@localhost + password: password + +// admin@localhost is assumed to have node creation privileges + +--------- + +Juliet connects + +-- Generated with https://gitlab.com/xmpp-rs/xmpp-parsers: +-- cargo run --example=generate-caps https://code.matthewwild.co.uk/scansion/ <<< "" +Juliet sends: + + + + 5a5oTk21S9EmWQGIyuMwPKuSkPwqmXv6aKO5ftqCw/Q= + f3ziwT4vDK+VxuWrhhPEEgI3HJcEw7Zg4MggYE6vjZ0= + + + +Juliet receives: + + + + +Juliet sends: + + + + + + + + +Juliet sends: + + + + + + JC + + + + + + + http://jabber.org/protocol/pubsub#publish-options + + + true + + + 255 + + + never + + + whitelist + + + + + + +Juliet receives: + + + + + + JC + + + + + + +Juliet receives: + + + + + + + + +Juliet sends: + + + + + + JC + + + + + + + http://jabber.org/protocol/pubsub#publish-options + + + true + + + 255 + + + never + + + whitelist + + + + + + +Juliet receives: + + + + + + JC + + + + + + +Juliet receives: + + + + + + + + +Juliet sends: + + + + + + + + +Juliet receives: + + + + + + + + +Juliet receives: + + +Juliet disconnects + +// vim: syntax=xml: diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_bookmarks2/tests/conversion.scs prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_bookmarks2/tests/conversion.scs --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_bookmarks2/tests/conversion.scs 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_bookmarks2/tests/conversion.scs 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,253 @@ +# Pubsub: Bookmarks 2.0 + +[Client] Juliet-old + jid: admin@localhost + password: password + +[Client] Juliet-new + jid: admin@localhost + password: password + +// admin@localhost is assumed to have node creation privileges + +--------- + +Juliet-new connects + +-- Generated with https://gitlab.com/xmpp-rs/xmpp-parsers: +-- cargo run --example=generate-caps https://code.matthewwild.co.uk/scansion/ <<< "" +Juliet-new sends: + + + + 5a5oTk21S9EmWQGIyuMwPKuSkPwqmXv6aKO5ftqCw/Q= + f3ziwT4vDK+VxuWrhhPEEgI3HJcEw7Zg4MggYE6vjZ0= + + + +Juliet-new receives: + + + + +Juliet-new sends: + + + + + + + + +Juliet-old connects + +Juliet-old sends: + + + + + + +Juliet-old receives: + + + + + + +Juliet-old sends: + + + + + JC + + + + + +Juliet-new receives: + + + + + + JC + + + + + + +Juliet-old receives: + + +Juliet-old sends: + + + + + + +Juliet-old receives: + + + + + JC + + + + + +Juliet-old sends: + + + + + JC + + + JC + + + + + +Juliet-new receives: + + + + + + JC + + + + + + +Juliet-old receives: + + +Juliet-old sends: + + + + + + +Juliet-old receives: + + + + + JC + + + JC + + + + + +Juliet-old sends: + + + + + JC + + + + + +Juliet-new receives: + + + + + + + + +Juliet-old receives: + + +Juliet-old sends: + + + + + + +Juliet-old receives: + + + + + JC + + + + + +Juliet-old sends: + + + + + + +Juliet-new receives: + + + + + + +Juliet-old receives: + + +Juliet-old sends: + + + + + + +Juliet-old receives: + + + + + + +Juliet-old disconnects + +Juliet-new disconnects + +// vim: syntax=xml: diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_conversejs/mod_conversejs.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_conversejs/mod_conversejs.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_conversejs/mod_conversejs.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_conversejs/mod_conversejs.lua 2020-01-28 09:32:42.000000000 +0000 @@ -72,6 +72,7 @@ domain_placeholder = module.host; allow_registration = allow_registration; registration_domain = allow_registration and module.host or nil; + view_mode = "fullscreen"; }; if type(user_options) == "table" then diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_csi_muc_priorities/mod_csi_muc_priorities.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_csi_muc_priorities/mod_csi_muc_priorities.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_csi_muc_priorities/mod_csi_muc_priorities.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_csi_muc_priorities/mod_csi_muc_priorities.lua 2020-01-28 09:32:42.000000000 +0000 @@ -7,37 +7,37 @@ local stanza, session = event.stanza, event.session; if stanza.name == "message" then if stanza.attr.type == "groupchat" then - local body = stanza:get_child_text("body"); - if not body then return end - local room_jid = jid_bare(stanza.attr.from); local username = session.username; local priorities = user_sessions[username].csi_muc_priorities; - if not priorities or priorities[room_jid] ~= false then - return nil; + if priorities then + local priority = priorities[room_jid]; + if priority ~= nil then + return priority; + end end -- Look for mention local rooms = session.rooms_joined; if rooms then + local body = stanza:get_child_text("body"); + if not body then return end local room_nick = rooms[room_jid]; if room_nick then if body:find(room_nick, 1, true) then return true; end + -- Your own messages if stanza.attr.from == (room_jid .. "/" .. room_nick) then return true; end end - elseif session.directed and session.directed[stanza.attr.from] then - -- fallback if no mod_track_muc_joins - return true; end - -- Unimportant and no mention - return false; + -- Standard importance and no mention, leave to other modules to decide for now + return nil; end end end); @@ -61,6 +61,12 @@ }; { type = "jid-multi"; + name = "important"; + label = "Higher priority"; + desc = "Group chats more important to you"; + }; + { + type = "jid-multi"; name = "unimportant"; label = "Lower priority"; desc = "E.g. large noisy public channels"; @@ -76,14 +82,23 @@ local adhoc_command_handler = adhoc_inital_data(priority_settings_form, function (data) local username = jid_split(data.from); local prioritized_jids = user_sessions[username].csi_muc_priorities or store:get(username); + local important = {}; local unimportant = {}; if prioritized_jids then - for jid in pairs(prioritized_jids) do - table.insert(unimportant, jid); + for jid, priority in pairs(prioritized_jids) do + if priority then + table.insert(important, jid); + else + table.insert(unimportant, jid); + end end + table.sort(important); table.sort(unimportant); end - return { unimportant = unimportant }; + return { + important = important; + unimportant = unimportant; +}; end, function(fields, form_err, data) if form_err then return { status = "completed", error = { message = "Problem in submitted form" } }; @@ -93,6 +108,9 @@ for _, jid in ipairs(fields.unimportant) do prioritized_jids[jid] = false; end + for _, jid in ipairs(fields.important) do + prioritized_jids[jid] = true; + end end local username = jid_split(data.from); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_easy_invite/mod_easy_invite.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_easy_invite/mod_easy_invite.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_easy_invite/mod_easy_invite.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_easy_invite/mod_easy_invite.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,203 @@ +-- XEP-0401: Easy User Onboarding +local dataforms = require "util.dataforms"; +local datetime = require "util.datetime"; +local jid_bare = require "util.jid".bare; +local jid_split = require "util.jid".split; +local split_jid = require "util.jid".split; +local rostermanager = require "core.rostermanager"; +local st = require "util.stanza"; + +local invite_only = module:get_option_boolean("registration_invite_only", true); +local require_encryption = module:get_option_boolean("c2s_require_encryption", + module:get_option_boolean("require_encryption", false)); + +local new_adhoc = module:require("adhoc").new; + +-- Whether local users can invite other users to create an account on this server +local allow_user_invites = module:get_option_boolean("allow_user_invites", true); + +local invites; +if prosody.shutdown then -- COMPAT hack to detect prosodyctl + invites = module:depends("invites"); +end + +local invite_result_form = dataforms.new({ + title = "Your Invite", + -- TODO instructions = something helpful + { + name = "uri"; + label = "Invite URI"; + -- TODO desc = something helpful + }, + { + name = "url" ; + var = "landing-url"; + label = "Invite landing URL"; + }, + { + name = "expire"; + label = "Token valid until"; + }, + }); + +module:depends("adhoc"); +module:provides("adhoc", new_adhoc("New Invite", "urn:xmpp:invite#invite", + function (_, data) + local username = split_jid(data.from); + local invite = invites.create_contact(username, allow_user_invites); + --TODO: check errors + return { + status = "completed"; + form = { + layout = invite_result_form; + values = { + uri = invite.uri; + url = invite.landing_page; + expire = datetime.datetime(invite.expires); + }; + }; + }; + end, "local_user")); + + +-- TODO +-- module:provides("adhoc", new_adhoc("Create account", "urn:xmpp:invite#create-account", function () end, "admin")); + +-- XEP-0379: Pre-Authenticated Roster Subscription +module:hook("presence/bare", function (event) + local stanza = event.stanza; + if stanza.attr.type ~= "subscribe" then return end + + local preauth = stanza:get_child("preauth", "urn:xmpp:pars:0"); + if not preauth then return end + local token = preauth.attr.token; + if not token then return end + + local username, host = jid_split(stanza.attr.to); + + local invite, err = invites.get(token, username); + + if not invite then + module:log("debug", "Got invalid token, error: %s", err); + return; + end + + local contact = jid_bare(stanza.attr.from); + + module:log("debug", "Approving inbound subscription to %s from %s", username, contact); + if rostermanager.set_contact_pending_in(username, host, contact, stanza) then + if rostermanager.subscribed(username, host, contact) then + invite:use(); + rostermanager.roster_push(username, host, contact); + + -- Send back a subscription request (goal is mutual subscription) + if not rostermanager.is_user_subscribed(username, host, contact) + and not rostermanager.is_contact_pending_out(username, host, contact) then + module:log("debug", "Sending automatic subscription request to %s from %s", contact, username); + if rostermanager.set_contact_pending_out(username, host, contact) then + rostermanager.roster_push(username, host, contact); + module:send(st.presence({type = "subscribe", to = contact })); + else + module:log("warn", "Failed to set contact pending out for %s", username); + end + end + end + end +end, 1); + +-- TODO sender side, magic automatic mutual subscription + +local invite_stream_feature = st.stanza("register", { xmlns = "urn:xmpp:invite" }):up(); +module:hook("stream-features", function(event) + local session, features = event.origin, event.features; + + -- Advertise to unauthorized clients only. + if session.type ~= "c2s_unauthed" or (require_encryption and not session.secure) then + return + end + + features:add_child(invite_stream_feature); +end); + +-- Client is submitting a preauth token to allow registration +module:hook("stanza/iq/urn:xmpp:pars:0:preauth", function(event) + local preauth = event.stanza.tags[1]; + local token = preauth.attr.token; + local validated_invite = invites.get(token); + if not validated_invite then + local reply = st.error_reply(event.stanza, "cancel", "forbidden", "The invite token is invalid or expired"); + event.origin.send(reply); + return true; + end + event.origin.validated_invite = validated_invite; + local reply = st.reply(event.stanza); + event.origin.send(reply); + return true; +end); + +-- Registration attempt - ensure a valid preauth token has been supplied +module:hook("user-registering", function (event) + local validated_invite = event.session.validated_invite; + if invite_only and not validated_invite then + event.allowed = false; + event.reason = "Registration on this server is through invitation only"; + return; + end +end); + +-- Make a *one-way* subscription. User will see when contact is online, +-- contact will not see when user is online. +function subscribe(host, user_username, contact_username) + local user_jid = user_username.."@"..host; + local contact_jid = contact_username.."@"..host; + -- Update user's roster to say subscription request is pending... + rostermanager.set_contact_pending_out(user_username, host, contact_jid); + -- Update contact's roster to say subscription request is pending... + rostermanager.set_contact_pending_in(contact_username, host, user_jid); + -- Update contact's roster to say subscription request approved... + rostermanager.subscribed(contact_username, host, user_jid); + -- Update user's roster to say subscription request approved... + rostermanager.process_inbound_subscription_approval(user_username, host, contact_jid); +end + +-- Make a mutual subscription between jid1 and jid2. Each JID will see +-- when the other one is online. +function subscribe_both(host, user1, user2) + subscribe(host, user1, user2); + subscribe(host, user2, user1); +end + +-- Registration successful, if there was a preauth token, mark it as used +module:hook("user-registered", function (event) + local validated_invite = event.session.validated_invite; + if not validated_invite then + return; + end + local inviter_username = validated_invite.inviter; + validated_invite:use(); + + if not inviter_username then return; end + + local contact_username = event.username; + + module:log("debug", "Creating mutual subscription between %s and %s", inviter_username, contact_username); + subscribe_both(module.host, inviter_username, contact_username); +end); + + +local sm = require "core.storagemanager"; +function module.command(arg) + if #arg < 2 or arg[2] ~= "generate" then + print("usage: prosodyctl mod_easy_invite example.net generate"); + return; + end + + local host = arg[1]; + assert(hosts[host], "Host "..tostring(host).." does not exist"); + sm.initialize_host(host); + + invites = module:context(host):depends("invites"); + local invite = invites.create_account(); + print(invite.uri); +end + diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_easy_invite/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_easy_invite/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_easy_invite/README.markdown 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_easy_invite/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,60 @@ + +This module allows admins and users to create invitations suitable for sharing +to potential new users/contacts. + +User invitations can be created through the "New Invite" ad-hoc command. An overview +of the semantics and protocol can be found at [modernxmpp.org/client/invites](https://docs.modernxmpp.org/client/invites/). + +This module depends on mod_invites to actually create and store the invitation tokens. + +# Configuration + +To allow users to join your server through invitations, you must +enable mod_register_ibr and set allow_registration = true, and then +also set `registration_invite_only = true` to restrict registration. + +| Name | Description | Default | +|--------------------------|-----------------------------------------------------------------------------------|---------| +| registration_invite_only | Whether registration attempts without an invite token should be blocked | true | +| allow_user_invites | Whether existing users should be allowed to invite new users to register accounts | true | + +## Example: Invite-only registration +``` {.lua} +-- To allow invitation through a token, mod_register +allow_registration = true +registration_invite_only = true +``` + +## Example: Open registration + +This setup allows completely open registration, even without +an invite token. + +``` {.lua} +allow_registration = true +registration_invite_only = false +``` + +## Invite creation permissions + +To allow existing users of your server to send invitation links that +allow new people to join your server, you can set `allow_user_invites = true`. + +If you do not wish users to invite other users to create accounts on your +server, set `allow_user_invites = false`. They will still be able to send +contact invites, but new contacts will be required to register an account +on a different server. + +# Usage + +Users can use the "New Invite" ad-hoc command through their client. + +Admins can create registration links using prosodyctl, e.g. + +``` +prosodyctl mod_easy_invite example.com generate +``` + +# Compatibility + +0.11 and later. diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_email/mod_email.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_email/mod_email.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_email/mod_email.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_email/mod_email.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,49 @@ +module:set_global(); + +local moduleapi = require "core.moduleapi"; + +local smtp = require"socket.smtp"; + +local config = module:get_option("smtp", { origin = "prosody", exec = "sendmail" }); + +local function send_email(to, headers, content) + if type(headers) == "string" then -- subject + headers = { + Subject = headers; + From = config.origin; + }; + end + headers.To = to; + if not headers["Content-Type"] then + headers["Content-Type"] = 'text/plain; charset="utf-8"'; + end + local message = smtp.message{ + headers = headers; + body = content; + }; + + if config.exec then + local pipe = io.popen(config.exec .. + " '"..to:gsub("'", "'\\''").."'", "w"); + + for str in message do + pipe:write(str); + end + + return pipe:close(); + end + + return smtp.send({ + user = config.user; password = config.password; + server = config.server; port = config.port; + domain = config.domain; + + from = config.origin; rcpt = to; + source = message; + }); +end + +assert(not moduleapi.send_email, "another email module is already loaded"); +function moduleapi:send_email(email) --luacheck: ignore 212/self + return send_email(email.to, email.headers or email.subject, email.body); +end diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_firewall/test.lib.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_firewall/test.lib.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_firewall/test.lib.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_firewall/test.lib.lua 2020-01-28 09:32:42.000000000 +0000 @@ -66,7 +66,7 @@ stderr(""); stderr(stats_dropped + stats_passed, "processed"); stderr(stats_passed, "passed"); - stderr(stats_dropped, "droppped"); + stderr(stats_dropped, "dropped"); stderr(line_count, "input lines"); stderr(""); end diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_index/mod_http_index.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_index/mod_http_index.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_index/mod_http_index.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_index/mod_http_index.lua 2020-01-28 09:32:42.000000000 +0000 @@ -3,6 +3,8 @@ module:depends"http"; +local show_all = module:get_option_boolean(module.name .. "_show_all", false); + local base_template; do local template_file = module:get_option_string(module.name .. "_template", module.name .. ".html"); @@ -28,7 +30,7 @@ local host_items = module:get_host_items("http-provider"); local http_apps = {} for _, item in ipairs(host_items) do - if module.name ~= item._provided_by then + if module.name ~= item._provided_by and (show_all or item.title) then table.insert(http_apps, { title = item.title or item.name; name = item.name; diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_index/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_index/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_index/README.markdown 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_index/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -18,7 +18,24 @@ -- other modules "http_index"; } +``` + +# Advanced + +## Listing all items + +By default only HTTP apps that include a human-readable title are +listed. This filtering can be disabled by setting: + +```lua +http_index_list_all = true +``` + +## Template + +The template can be customized by copying the included `http_index.html` +and pointing to it with the `http_index_template` setting: --- optional, defaults to a file next to the module +``` lua http_index_template = "/path/to/template.html" ``` diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_muc_log/http_muc_log.html prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_muc_log/http_muc_log.html --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_muc_log/http_muc_log.html 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_muc_log/http_muc_log.html 2020-01-28 09:32:42.000000000 +0000 @@ -70,7 +70,7 @@
{item.name}
{item.description?}
} -{years# +{dates|calendarize#

{item.year}

{item.months# diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_muc_log/mod_http_muc_log.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_muc_log/mod_http_muc_log.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_muc_log/mod_http_muc_log.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_muc_log/mod_http_muc_log.lua 2020-01-28 09:32:42.000000000 +0000 @@ -6,7 +6,8 @@ local url = require"socket.url"; local os_time, os_date = os.time, os.date; local httplib = require "util.http"; -local render = require"util.interpolation".new("%b{}", require"util.stanza".xml_escape); +local render_funcs = {}; +local render = require"util.interpolation".new("%b{}", require"util.stanza".xml_escape, render_funcs); local archive = module:open_store("muc_log", "archive"); @@ -147,58 +148,54 @@ return false; end --- Produce the calendar view -local function years_page(event, path) - local request, response = event.request, event.response; - - local room = nodeprep(path:match("^(.*)/$")); - local is_open = open_room(room); - if is_open == nil then - return -- implicit 404 - elseif is_open == false then - return 403; - end - - -- Collect each date that has messages - -- convert it to a year / month / day tree +local function get_dates(room) --> { integer, ... } local date_list = archive.dates and archive:dates(room); - local dates = mt.new(); if date_list then - for _, date in ipairs(date_list) do - local when = datetime.parse(date.."T00:00:00Z"); - local t = os_date("!*t", when); - dates:set(t.year, t.month, t.day, when); + for i = 1, #date_list do + date_list[i] = datetime.parse(date_list[i].."T00:00:00Z"); end - elseif lazy then + return date_list; + end + + if lazy then -- Lazy with many false positives + date_list = {}; local first_day = find_once(room, nil, 3); local last_day = find_once(room, { reverse = true }, 3); if first_day and last_day then first_day = date_floor(first_day); last_day = date_floor(last_day); for when = first_day, last_day, 86400 do - local t = os_date("!*t", when); - dates:set(t.year, t.month, t.day, when); + table.insert(date_list, when); end else return; -- 404 end - else - -- Collect date the hard way - module:log("debug", "Find all dates with messages"); - local next_day; - repeat - local when = find_once(room, { start = next_day; }, 3); - if not when then break; end - local t = os_date("!*t", when); - dates:set(t.year, t.month, t.day, when ); - next_day = date_floor(when) + 86400; - until not next_day; + return date_list; end - local years = {}; + -- Collect date the hard way + module:log("debug", "Find all dates with messages"); + date_list = {}; + local next_day; + repeat + local when = find_once(room, { start = next_day; }, 3); + if not when then break; end + table.insert(date_list, when); + next_day = date_floor(when) + 86400; + until not next_day; + return date_list; +end +function render_funcs.calendarize(date_list) + -- convert array of timestamps to a year / month / day tree + local dates = mt.new(); + for _, when in ipairs(date_list) do + local t = os_date("!*t", when); + dates:set(t.year, t.month, t.day, when); + end -- Wrangle Y/m/d tree into year / month / week / day tree for calendar view + local years = {}; for current_year, months_t in pairs(dates.data) do local t = { year = current_year, month = 1, day = 1 }; local months = { }; @@ -232,9 +229,28 @@ current_day = current_day+1; end end - table.sort(year, sort_m); + table.sort(months, sort_m); end table.sort(years, sort_Y); + return years; +end + +-- Produce the calendar view +local function years_page(event, path) + local request, response = event.request, event.response; + + local room = nodeprep(path:match("^(.*)/$")); + local is_open = open_room(room); + if is_open == nil then + return -- implicit 404 + elseif is_open == false then + return 403; + end + + local date_list = get_dates(room); + if not date_list then + return; -- 404 + end -- Phew, all wrangled, all that's left is rendering it with the template @@ -245,7 +261,7 @@ jid_node = jid_split(get_room(room).jid); hide_presence = hide_presence(request); presence_available = presence_logged; - years = years; + dates = date_list; links = { { href = "../", rel = "up", text = "Room list" }, { href = "latest", rel = "last", text = "Latest" }, @@ -385,6 +401,7 @@ lang = get_room(room).get_language and get_room(room):get_language(); lines = logs; links = links; + dates = {}; -- COMPAT util.interpolation {nil|func#...} bug }); end @@ -414,10 +431,12 @@ hide_presence = hide_presence(request); presence_available = presence_logged; rooms = room_list; + dates = {}; -- COMPAT util.interpolation {nil|func#...} bug }); end module:provides("http", { + title = module:get_option_string("name", "Chatroom logs"); route = { ["GET /"] = list_rooms; ["GET /*"] = logs_page; diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_upload/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_upload/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_http_upload/README.markdown 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_http_upload/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -22,7 +22,7 @@ Component "upload.example.org" "http_upload" ``` -Alternatively it can be added to `modules_enabled` like other modules. +It should **not** be added to modules_enabled. Limits ------ diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_invite/mod_invite.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_invite/mod_invite.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_invite/mod_invite.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_invite/mod_invite.lua 2020-01-28 09:32:42.000000000 +0000 @@ -156,6 +156,6 @@ return { info = module:http_url() .. "/" .. uuid, status = "completed" }; end -local adhoc_invite = adhoc_new("Invite user", "invite", invite_command_handler, "user") +local adhoc_invite = adhoc_new("Invite user", "invite", invite_command_handler, "local_user") -module:add_item("adhoc", adhoc_invite); \ No newline at end of file +module:add_item("adhoc", adhoc_invite); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_invites/mod_invites.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_invites/mod_invites.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_invites/mod_invites.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_invites/mod_invites.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,129 @@ +local id = require "util.id"; +local url = require "socket.url"; +local jid_node = require "util.jid".node; + +local invite_ttl = module:get_option_number("invite_expiry", 86400 * 7); + +local token_storage = module:open_store("invite_token", "map"); + +local function get_uri(action, jid, token, params) --> string + return url.build({ + scheme = "xmpp", + path = jid, + query = action..";preauth="..token..(params and (";"..params) or ""), + }); +end + +local function create_invite(invite_action, invite_jid, allow_registration) + local token = id.medium(); + + local created_at = os.time(); + local expires = created_at + invite_ttl; + + local invite_params = (invite_action == "roster" and allow_registration) and "ibr=y" or nil; + + local invite = { + type = invite_action; + jid = invite_jid; + + token = token; + allow_registration = allow_registration; + + uri = get_uri(invite_action, invite_jid, token, invite_params); + + created_at = created_at; + expires = expires; + }; + + module:fire_event("invite-created", invite); + + if allow_registration then + local ok, err = token_storage:set(nil, token, invite); + if not ok then + module:log("warn", "Failed to store account invite: %s", err); + return nil, "internal-server-error"; + end + end + + if invite_action == "roster" then + local username = jid_node(invite_jid); + local ok, err = token_storage:set(username, token, expires); + if not ok then + module:log("warn", "Failed to store subscription invite: %s", err); + return nil, "internal-server-error"; + end + end + + return invite; +end + +-- Create invitation to register an account (optionally restricted to the specified username) +function create_account(account_username) --luacheck: ignore 131/create_account + local jid = account_username and (account_username.."@"..module.host) or module.host; + return create_invite("register", jid, true); +end + +-- Create invitation to become a contact of a local user +function create_contact(username, allow_registration) --luacheck: ignore 131/create_contact + return create_invite("roster", username.."@"..module.host, allow_registration); +end + +local valid_invite_methods = {}; +local valid_invite_mt = { __index = valid_invite_methods }; + +function valid_invite_methods:use() + if self.username then + -- Also remove the contact invite if present, on the + -- assumption that they now have a mutual subscription + token_storage:set(self.username, self.token, nil); + end + token_storage:set(nil, self.token, nil); + return true; +end + +-- Get a validated invite (or nil, err). Must call :use() on the +-- returned invite after it is actually successfully used +-- For "roster" invites, the username of the local user (who issued +-- the invite) must be passed. +-- If no username is passed, but the registration is a roster invite +-- from a local user, the "inviter" field of the returned invite will +-- be set to their username. +function get(token, username) + if not token then + return nil, "no-token"; + end + + local valid_until, inviter; + + if username then -- token being used for subscription + -- Fetch from user store (subscription invite) + valid_until = token_storage:get(username, token); + else -- token being used for account creation + -- Fetch from host store (account invite) + local token_info = token_storage:get(nil, token); + valid_until = token_info and token_info.expires; + if token_info.type == "roster" then + username = jid_node(token_info.jid); + inviter = username; + end + end + + if not valid_until then + module:log("debug", "Got unknown token: %s", token); + return nil, "token-invalid"; + elseif os.time() > valid_until then + module:log("debug", "Got expired token: %s", token); + return nil, "token-expired"; + end + + return setmetatable({ + token = token; + username = username; + inviter = inviter; + }, valid_invite_mt); +end + +function use(token) --luacheck: ignore 131/use + local invite = get(token); + return invite and invite:use(); +end diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_invites/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_invites/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_invites/README.markdown 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_invites/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,22 @@ + +This module manages the creation and consumption of invite codes for the +host(s) it is loaded onto. It currently does not expose any admin/user-facing +functionality (though in the future it will probably gain a way to view/manage +pending invites). + +Other modules can use the API from this module to create invite tokens which +can be used to e.g. register accounts or create automatic subscription approvals. + +# Configuration + +``` {.lua} +-- Configure the number of seconds a token is valid for (default 7 days) +invite_expiry = 86400 * 7 +``` + +Note that all modules that use this API will automatically load this module, +so adding it to modules_enabled is generally not necessary. + +# Compatibility + +0.11 and later. diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_log_json/mod_log_json.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_log_json/mod_log_json.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_log_json/mod_log_json.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_log_json/mod_log_json.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,50 @@ +local pack = require "util.table".pack; +local json = require "util.json"; +local array = require "util.array"; +local datetime = require "util.datetime".datetime; +local socket = require "socket"; + +module:set_global(); + +local function sink_maker(config) + local send = function () end + if config.filename then + local logfile = io.open(config.filename, "a+"); + logfile:setvbuf("no"); + function send(payload) + logfile:write(payload, "\n"); + end + elseif config.udp_host and config.udp_port then + local conn = socket.udp(); + conn:setpeername(config.udp_host, config.udp_port); + function send(payload) + conn:send(payload); + end + end + return function (source, level, message, ...) + local args = pack(...); + for i = 1, args.n do + if args[i] == nil then + args[i] = json.null; + elseif type(args[i]) ~= "string" or type(args[i]) ~= "number" then + args[i] = tostring(args[i]); + end + end + args.n = nil; + local payload = { + datetime = datetime(), + source = source, + level = level, + message = message, + args = array(args); + }; + send(json.encode(payload)); + end +end + +function module.unload() + -- deregister + require"core.loggingmanager".register_sink_type("json", nil); +end + +require"core.loggingmanager".register_sink_type("json", sink_maker); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_log_json/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_log_json/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_log_json/README.markdown 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_log_json/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,72 @@ +--- +summary: JSON Log Sink +--- + +Conifiguration +============== + +Here we log to `/var/log/prosody/prosody.json`: + +``` {.lua} +log = { + -- your other log sinks + info = "/var/log/prosody/prosody.log" + -- add this: + { to = "json", filename = "/var/log/prosody/prosody.json" }; +} +``` + +## UDP + +Alternatively, it can send logs via UDP: + +```lua +log = { + { to = "json", udp_host = "10.1.2.3", udp_port = "9999" }; +} +``` + +Format +====== + +JSON log files consist of a series of `\n`-separated JSON objects, +suitable for mangling with tools like +[`jq`](https://stedolan.github.io/jq/). + +Example (with whitespace and indentation for readability): + +``` {.json} +{ + "args" : [], + "datetime" : "2019-11-03T13:38:28Z", + "level" : "info", + "message" : "Client connected", + "source" : "c2s55f267f5b9d0" +} +{ + "args" : [ + "user@example.net" + ], + "datetime" : "2019-11-03T13:38:28Z", + "level" : "debug", + "message" : "load_roster: asked for: %s", + "source" : "rostermanager" +} +``` + +`datetime` +: [XEP-0082]-formatted timestamp. + +`source` +: Log source, usually a module or a connected session. + +`level` +: `debug`, `info`, `warn` or `error` + +`message` +: The log message in `printf` format. Combine with `args` to get the + final message. + +`args` +: Array of extra arguments, corresponding to `printf`-style `%s` + formatting in the `message`. diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_measure_message_e2ee/mod_measure_message_e2ee.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_measure_message_e2ee/mod_measure_message_e2ee.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_measure_message_e2ee/mod_measure_message_e2ee.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_measure_message_e2ee/mod_measure_message_e2ee.lua 2020-01-28 09:32:42.000000000 +0000 @@ -1,5 +1,3 @@ -module:set_global(); - local count_message = module:measure("message", "rate"); local count_plain = module:measure("plain", "rate"); local count_openpgp = module:measure("openpgp", "rate"); @@ -45,9 +43,6 @@ end end -function module.add_host(host_module) - module:log("debug", "Loaded on host %s", host_module); - host_module:hook("pre-message/host", message_handler, 2); - host_module:hook("pre-message/bare", message_handler, 2); - host_module:hook("pre-message/full", message_handler, 2); -end +module:hook("pre-message/host", message_handler, 2); +module:hook("pre-message/bare", message_handler, 2); +module:hook("pre-message/full", message_handler, 2); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_muc_occupant_id/mod_muc_occupant_id.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_muc_occupant_id/mod_muc_occupant_id.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_muc_occupant_id/mod_muc_occupant_id.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_muc_occupant_id/mod_muc_occupant_id.lua 2020-01-28 09:32:42.000000000 +0000 @@ -10,43 +10,32 @@ local xmlns_occupant_id = "urn:xmpp:occupant-id:0"; -local function edit_occupant(event) - local occupant, room = event.occupant, event.room; +local function generate_id(occupant, room) local bare = occupant.bare_jid; - -- TODO: Move the salt on the MUC component. Setting the salt on the room - -- can be problematic when the room is destroyed. Next time it's recreated - -- the salt will be different and so will be the unique_id. Or maybe we want - -- this anyway? if room._data.occupant_id_salt == nil then - local salt = uuid.generate(); - room._data.occupant_id_salt = salt; + room._data.occupant_id_salt = uuid.generate(); end - local unique_id = b64encode(hmac_sha256(bare, room._data.occupant_id_salt)); - - -- TODO: Store this only once per bare jid and not once per occupant? - local stanza = event.stanza; - stanza:tag("occupant-id", { xmlns = xmlns_occupant_id }) - :text(unique_id) - :up(); -end - -local function handle_stanza(event) - local stanza, occupant = event.stanza, event.occupant; + if room._data.occupant_ids == nil then + room._data.occupant_ids = {}; + end - if stanza.name == "presence" and stanza.attr.type == "unavailable" then -- not required here - return; + if room._data.occupant_ids[bare] == nil then + local unique_id = b64encode(hmac_sha256(bare, room._data.occupant_id_salt)); + room._data.occupant_ids[bare] = unique_id; end - -- TODO: Handle MAM. + return room._data.occupant_ids[bare]; +end + +local function update_occupant(event) + local stanza, occupant, room = event.stanza, event.occupant, event.room; -- strip any existing tags to avoid forgery stanza:remove_children("occupant-id", xmlns_occupant_id); - local unique_id = occupant.sessions[stanza.attr.from] - :get_child("occupant-id", xmlns_occupant_id) - :get_text(); + local unique_id = generate_id(occupant, room); stanza:tag("occupant-id", { xmlns = xmlns_occupant_id }) :text(unique_id) :up(); @@ -57,5 +46,7 @@ event.reply:tag("feature", { var = xmlns_occupant_id }):up(); end); -module:hook("muc-occupant-pre-join", edit_occupant); -module:hook("muc-occupant-groupchat", handle_stanza); +-- TODO: Handle MUC-PMs +module:hook("muc-broadcast-presence", update_occupant); +module:hook("muc-occupant-pre-join", update_occupant); +module:hook("muc-occupant-groupchat", update_occupant); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_nodeinfo2/mod_nodeinfo2.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_nodeinfo2/mod_nodeinfo2.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_nodeinfo2/mod_nodeinfo2.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_nodeinfo2/mod_nodeinfo2.lua 2020-01-28 09:32:42.000000000 +0000 @@ -1,58 +1,94 @@ local json = require "util.json"; local array = require "util.array"; +local add_task = require "util.timer".add_task; local get_stats = require "core.statsmanager".get_stats; +local list_users = require "core.usermanager".users; local os_time = os.time; module:depends("http"); -module:depends("lastlog"); -module:depends("measure_message_e2ee"); -local store = module:open_store("lastlog"); +local expose_users = module:get_option_boolean("nodeinfo2_expose_users", true); +if expose_users then + module:depends("lastlog"); +end + +local expose_posts = module:get_option_boolean("nodeinfo2_expose_posts", true); +if expose_posts then + module:depends("measure_message_e2ee"); +end + +local main_store = module:open_store(); +local lastlog_store = module:open_store("lastlog"); + +local data; +if expose_posts then + data = main_store:get("nodeinfo2") or { message_count = 0 }; +end local total_users = 0; -local half_year_users = 0; -local month_users = 0; local week_users = 0; -for user in require "core.usermanager".users(module.host) do -- TODO refresh at some interval? - total_users = total_users + 1; - local lastlog = store:get(user); - if lastlog and lastlog.timestamp then - local delta = os_time() - lastlog.timestamp; - if delta < 6 * 30 * 24 * 60 * 60 then - half_year_users = half_year_users + 1; - end - if delta < 30 * 24 * 60 * 60 then - month_users = month_users + 1; - end - if delta < 7 * 24 * 60 * 60 then - week_users = week_users + 1; +local month_users = 0; +local half_year_users = 0; + +local function update_user_list() + for user in list_users(module.host) do + total_users = total_users + 1; + local lastlog = lastlog_store:get(user); + if lastlog and lastlog.timestamp then + local delta = os_time() - lastlog.timestamp; + if delta < 7 * 86400 then + week_users = week_users + 1; + end + if delta < 30 * 86400 then + month_users = month_users + 1; + end + if delta < 6 * 30 * 86400 then + half_year_users = half_year_users + 1; + end end end -end --- Remove the properties if we couldn’t find a single active user. It most likely means mod_lastlog isn’t in use. -if half_year_users == 0 and month_users == 0 and week_users == 0 then - half_year_users = nil; - month_users = nil; - week_users = nil; + -- Remove the properties if we couldn’t find a single active user. It most likely means mod_lastlog isn’t in use. + if half_year_users == 0 and month_users == 0 and week_users == 0 then + week_users = nil; + month_users = nil; + half_year_users = nil; + end end -local message_count_store = module:open_store("message_count"); -local message_count = message_count_store:get("message_count"); +if expose_users then + add_task(86400, update_user_list); + update_user_list(); +end module:provides("http", { default_path = "/.well-known/x-nodeinfo2"; route = { GET = function (event) - local stats, changed_only, extras = get_stats(); - for stat, _ in pairs(stats) do - if stat == "/*/mod_measure_message_e2ee/message:rate" then - local new_message_count = extras[stat].total; - if new_message_count ~= message_count then - message_count = new_message_count; - message_count_store:set("message_count", message_count); + local usage = {}; + if expose_users then + usage.users = { + total = total_users; + activeWeek = week_users; + activeMonth = month_users; + activeHalfyear = half_year_users; + }; + end + + if expose_posts then + local stats, changed_only, extras = get_stats(); + for stat, _ in pairs(stats) do + if stat == "/"..module.host.."/mod_measure_message_e2ee/message:rate" then + local new_message_count = extras[stat].total; + if new_message_count ~= data.message_count then + data = { message_count = new_message_count }; + main_store:set("nodeinfo2", data); + end end end + usage.localPosts = data.message_count; + -- TODO: also count PubSub replies here. + usage.localComments = 0; end event.response.headers.content_type = "application/json"; @@ -74,26 +110,16 @@ protocols = array { "xmpp", }; - --[[ TODO would be cool to identify local transports services = { inbound = array { - "irc"; + "xmpp"; }; outbound = array { + "xmpp"; }; }; - --]] openRegistrations = module:get_option_boolean("allow_registration", false); - usage = { - users = { - total = total_users; - activeHalfyear = half_year_users; - activeMonth = month_users; - activeWeek = week_users; - }; - localPosts = message_count; - localComments = message_count; - }; + usage = usage; }); end; } diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_nodeinfo2/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_nodeinfo2/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_nodeinfo2/README.markdown 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_nodeinfo2/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,48 @@ +--- +description: +labels: 'Stage-Alpha' +--- + +Introduction +============ + +This module exposes a [nodeinfo2](https://git.feneas.org/jaywink/nodeinfo2) +.well-known URL for use e.g. from +[the-federation.info](https://the-federation.info). + +Configuration +============= + +Enable the `nodeinfo` module in your global `modules_enabled` section: +``` +modules_enabled = { + ... + "nodeinfo2" + ... +} +``` + +Set the `nodeinfo2_expose_users` option to false if you don’t want to expose +statistics about the amount of users you host: +``` +nodeinfo2_expose_users = false +``` + +Set the `nodeinfo2_expose_posts` option to false if you don’t want to expose +statistics about the amount of messages being exchanged by your users: +``` +nodeinfo2_expose_posts = false +``` + +This module depends on +[mod\_lastlog](https://modules.prosody.im/mod_lastlog.html) to calculate user +activity, and [mod\_http](https://prosody.im/doc/http). Most of its +configuration actually happens in this dependency. + +Compatibility +============= + + ----- ----------- + trunk Works + 0.11 Should work + ----- ----------- diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_pastebin/mod_pastebin.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_pastebin/mod_pastebin.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_pastebin/mod_pastebin.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_pastebin/mod_pastebin.lua 2020-01-28 09:32:42.000000000 +0000 @@ -155,10 +155,10 @@ module:hook("muc-disco#info", function (event) local reply, form, formdata = event.reply, event.form, event.formdata; reply:tag("feature", { var = "https://modules.prosody.im/mod_pastebin" }):up(); - table.insert(form, { name = "https://modules.prosody.im/mod_pastebin#max_lines", datatype = "xs:integer" }); - table.insert(form, { name = "https://modules.prosody.im/mod_pastebin#max_characters", datatype = "xs:integer" }); - formdata["https://modules.prosody.im/mod_pastebin#max_lines"] = tostring(line_threshold); - formdata["https://modules.prosody.im/mod_pastebin#max_characters"] = tostring(length_threshold); + table.insert(form, { name = "{https://modules.prosody.im/mod_pastebin}max_lines", datatype = "xs:integer" }); + table.insert(form, { name = "{https://modules.prosody.im/mod_pastebin}max_characters", datatype = "xs:integer" }); + formdata["{https://modules.prosody.im/mod_pastebin}max_lines"] = tostring(line_threshold); + formdata["{https://modules.prosody.im/mod_pastebin}max_characters"] = tostring(length_threshold); end); function expire_pastes(time) diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_presence_cache/mod_presence_cache.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_presence_cache/mod_presence_cache.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_presence_cache/mod_presence_cache.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_presence_cache/mod_presence_cache.lua 2020-01-28 09:32:42.000000000 +0000 @@ -103,7 +103,7 @@ local function clear_cache_from_s2s(remote, reason) if not remote then return end - if reason and reason:find("timeout") then return end -- Ignore connections closed for being idle + -- FIXME Ignore if connection closed for being idle module:log("debug", "Dropping cached presence from host %s", remote); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_pubsub_post/mod_pubsub_post.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_pubsub_post/mod_pubsub_post.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_pubsub_post/mod_pubsub_post.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_pubsub_post/mod_pubsub_post.lua 2020-01-28 09:32:42.000000000 +0000 @@ -43,7 +43,7 @@ return { status_code = 400; body = "object or array expected"; }; end local wrapper = st.stanza("json", { xmlns="urn:xmpp:json:0" }):text(data); - return publish_payload(node, actor, data.id or "current", wrapper); + return publish_payload(node, actor, type(parsed.id) == "string" and parsed.id or "current", wrapper); end local function publish_atom(node, actor, feed) diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_register_web/mod_register_web.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_register_web/mod_register_web.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_register_web/mod_register_web.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_register_web/mod_register_web.lua 2020-01-28 09:32:42.000000000 +0000 @@ -198,6 +198,7 @@ end module:provides("http", { + title = module:get_option_string("register_web_title", "Account Registration"); route = { GET = generate_page; ["GET /"] = generate_page; diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_reload_modules/mod_reload_modules.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_reload_modules/mod_reload_modules.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_reload_modules/mod_reload_modules.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_reload_modules/mod_reload_modules.lua 2020-01-28 09:32:42.000000000 +0000 @@ -28,6 +28,12 @@ module:log("debug", "Reloading %s", module_name); mm.reload(module.host, module_name); end + + local global_modules = module:get_option_set("reload_global_modules", {}); + for module_name in global_modules do + module:log("debug", "Global reload of mod_%s", module_name); + mm.reload("*", module_name); + end end diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/example/app.py prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/example/app.py --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/example/app.py 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/example/app.py 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,73 @@ +from flask import Flask, Response, request, jsonify + +app = Flask("echobot") + + +@app.route("/api", methods=["OPTIONS"]) +def options(): + """ + Startup check. Return an appropriate Accept header to confirm the + data type to use. + """ + + return Response(status=200, headers={"accept": "application/json"}) + + +@app.route("/api", methods=["POST"]) +def hello(): + """ + Example RESTful JSON format stanza handler. + """ + + print(request.data) + if request.is_json: + data = request.get_json() + + if "kind" not in data: + return Response(status=400) + + if data["kind"] == "message" and "body" in data: + # Reply to a message + return jsonify({"body": "Yes this is flask app"}) + + elif data["kind"] == "iq" and data["type"] == "get": + if "ping" in data: + # Respond to ping + return Response(status=204) + + elif "disco" in data: + # Return supported features + return jsonify( + { + "disco": { + "identities": [ + { + "category": "component", + "type": "generic", + "name": "Flask app", + } + ], + "features": [ + "http://jabber.org/protocol/disco#info", + "http://jabber.org/protocol/disco#items", + "urn:xmpp:ping", + ], + } + } + ) + + elif "items" in data: + # Disco items + return jsonify( + {"items": [{"jid": "example.org", "name": "Example Dot Org"}]} + ) + + elif "version" in data: + # Version info + return jsonify({"version": {"name": "app.py", "version": "0"}}) + + return Response(status=501) + + +if __name__ == "__main__": + app.run() diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/jsonmap.lib.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/jsonmap.lib.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/jsonmap.lib.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/jsonmap.lib.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,317 @@ +local array = require "util.array"; +local jid = require "util.jid"; +local json = require "util.json"; +local st = require "util.stanza"; +local xml = require "util.xml"; + +local simple_types = { + -- basic message + body = "text_tag", + subject = "text_tag", + thread = "text_tag", + + -- basic presence + show = "text_tag", + status = "text_tag", + priority = "text_tag", + + state = {"name", "http://jabber.org/protocol/chatstates"}, + nick = {"text_tag", "http://jabber.org/protocol/nick", "nick"}, + delay = {"attr", "urn:xmpp:delay", "delay", "stamp"}, + replace = {"attr", "urn:xmpp:message-correct:0", "replace", "id"}, + + -- XEP-0045 MUC + -- TODO history, password, ??? + join = {"bool_tag", "http://jabber.org/protocol/muc", "x"}, + + -- XEP-0071 + html = { + "func", "http://jabber.org/protocol/xhtml-im", "html", + function (s) --> json string + return (tostring(s:get_child("body", "http://www.w3.org/1999/xhtml")):gsub(" xmlns='[^']*'","", 1)); + end; + function (s) --> xml + if type(s) == "string" then + return assert(xml.parse([[]]..s..[[]])); + end + end; + }; + + -- XEP-0199: XMPP Ping + ping = {"bool_tag", "urn:xmpp:ping", "ping"}, + + -- XEP-0092: Software Version + version = {"func", "jabber:iq:version", "query", + function (s) + return { + name = s:get_child_text("name"); + version = s:get_child_text("version"); + os = s:get_child_text("os"); + } + end, + function (s) + local v = st.stanza("query", { xmlns = "jabber:iq:version" }); + if type(s) == "table" then + v:text_tag("name", s.name); + v:text_tag("version", s.version); + if s.os then + v:text_tag("os", s.os); + end + end + return v; + end + }; + + -- XEP-0030 + disco = { + "func", "http://jabber.org/protocol/disco#info", "query", + function (s) --> array of features + local identities, features = array(), array(); + for tag in s:childtags() do + if tag.name == "identity" and tag.attr.category and tag.attr.type then + identities:push({ category = tag.attr.category, type = tag.attr.type, name = tag.attr.name }); + elseif tag.name == "feature" and tag.attr.var then + features:push(tag.attr.var); + end + end + return { node = s.attr.node, identities = identities, features = features, }; + end; + function (s) + if type(s) == "table" and s ~= json.null then + local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s.node }); + if s.identities then + for _, identity in ipairs(s.identities) do + disco:tag("identity", { category = identity.category, type = identity.type, name = identity.name }):up(); + end + end + if s.features then + for _, feature in ipairs(s.features) do + disco:tag("feature", { var = feature }):up(); + end + end + return disco; + else + st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", }); + end + end; + }; + + items = { + "func", "http://jabber.org/protocol/disco#items", "query", + function (s) --> array of features + local items = array(); + for item in s:childtags("item") do + items:push({ jid = item.attr.jid, node = item.attr.node, name = item.attr.name }); + end + return items; + end; + function (s) + local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items" }); + if type(s) == "table" and s ~= json.null then + for _, item in ipairs(s) do + if type(item) == "string" then + disco:tag("item", { jid = item }); + elseif type(item) == "table" then + disco:tag("item", { jid = item.jid, node = item.node, name = item.name }); + end + end + end + return disco; + end; + }; + + -- XEP-0066: Out of Band Data + oob_url = {"func", "jabber:iq:oob", "query", + function (s) + return s:get_child_text("url"); + end; + function (s) + if type(s) == "string" then + return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s); + end + end; + }; + + -- XEP-XXXX: User-defined Data Transfer + payload = {"func", "urn:xmpp:udt:0", "payload", + function (s) + local rawjson = s:get_child_text("json", "urn:xmpp:json:0"); + if not rawjson then return nil, "missing-json-payload"; end + local parsed, err = json.decode(rawjson); + if not parsed then return nil, err; end + return { + datatype = s.attr.datatype; + data = parsed; + }; + end; + function (s) + if type(s) == "table" then + return st.stanza("payload", { xmlns = "urn:xmpp:udt:0", datatype = s.datatype }) + :tag("json", { xmlns = "urn:xmpp:json:0" }):text(json.encode(s.data)); + end; + end + }; + +}; + +local implied_kinds = { + disco = "iq", + items = "iq", + ping = "iq", + version = "iq", + + body = "message", + html = "message", + replace = "message", + state = "message", + subject = "message", + thread = "message", + + join = "presence", + priority = "presence", + show = "presence", + status = "presence", +} + +local kind_by_type = { + get = "iq", set = "iq", result = "iq", + normal = "message", chat = "message", headline = "message", groupchat = "message", + available = "presence", unavailable = "presence", + subscribe = "presence", unsubscribe = "presence", + subscribed = "presence", unsubscribed = "presence", +} + +local function st2json(s) + local t = { + kind = s.name, + type = s.attr.type, + to = s.attr.to, + from = s.attr.from, + id = s.attr.id, + }; + if s.name == "presence" and not s.attr.type then + t.type = "available"; + end + + if t.to then + t.to = jid.prep(t.to); + if not t.to then return nil, "invalid-jid-to"; end + end + if t.from then + t.from = jid.prep(t.from); + if not t.from then return nil, "invalid-jid-from"; end + end + + if t.type == "error" then + local err_typ, err_condition, err_text = s:get_error(); + t.error = { + type = err_typ, + condition = err_condition, + text = err_text + }; + return t; + end + + for k, typ in pairs(simple_types) do + if typ == "text_tag" then + t[k] = s:get_child_text(k); + elseif typ[1] == "text_tag" then + t[k] = s:get_child_text(typ[3], typ[2]); + elseif typ[1] == "name" then + local child = s:get_child(nil, typ[2]); + if child then + t[k] = child.name; + end + elseif typ[1] == "attr" then + local child = s:get_child(typ[3], typ[2]) + if child then + t[k] = child.attr[typ[4]]; + end + elseif typ[1] == "bool_tag" then + if s:get_child(typ[3], typ[2]) then + t[k] = true; + end + elseif typ[1] == "func" then + local child = s:get_child(typ[3], typ[2] or k); + -- TODO handle err + if child then + t[k] = typ[4](child); + end + end + end + + return t; +end + +local function str(s) + if type(s) == "string" then + return s; + end +end + +local function json2st(t) + if type(t) ~= "table" or not str(next(t)) then + return nil, "invalid-json"; + end + local kind = str(t.kind) or kind_by_type[str(t.type)]; + if not kind then + for k, implied in pairs(implied_kinds) do + if t[k] then + kind = implied; + break + end + end + end + + local s = st.stanza(kind or "message", { + type = t.type ~= "available" and str(t.type) or nil, + to = str(t.to) and jid.prep(t.to); + from = str(t.to) and jid.prep(t.from); + id = str(t.id), + }); + + if t.to and not s.attr.to then + return nil, "invalid-jid-to"; + end + if t.from and not s.attr.from then + return nil, "invalid-jid-from"; + end + if kind == "iq" and not s.attr.type then + s.attr.type = "get"; + end + + if type(t.error) == "table" then + return st.error_reply(st.reply(s), str(t.error.type), str(t.error.condition), str(t.error.text)); + elseif t.type == "error" then + s:text_tag("error", t.body, { code = t.error_code and tostring(t.error_code) }); + return s; + end + + for k, v in pairs(t) do + local typ = simple_types[k]; + if typ then + if typ == "text_tag" then + s:text_tag(k, v); + elseif typ[1] == "text_tag" then + s:text_tag(typ[3] or k, v, typ[2] and { xmlns = typ[2] }); + elseif typ[1] == "name" then + s:tag(v, { xmlns = typ[2] }):up(); + elseif typ[1] == "attr" then + s:tag(typ[3] or k, { xmlns = typ[2], [ typ[4] or k ] = v }):up(); + elseif typ[1] == "bool_tag" then + s:tag(typ[3] or k, { xmlns = typ[2] }):up(); + elseif typ[1] == "func" then + s:add_child(typ[5](v)):up(); + end + end + end + + s:reset(); + + return s; +end + +return { + st2json = st2json; + json2st = json2st; +}; diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/mod_rest.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/mod_rest.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/mod_rest.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/mod_rest.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,331 @@ +-- RESTful API +-- +-- Copyright (c) 2019-2020 Kim Alvefur +-- +-- This file is MIT/X11 licensed. + +local errors = require "util.error"; +local http = require "net.http"; +local id = require "util.id"; +local jid = require "util.jid"; +local json = require "util.json"; +local st = require "util.stanza"; +local xml = require "util.xml"; + +local allow_any_source = module:get_host_type() == "component"; +local validate_from_addresses = module:get_option_boolean("validate_from_addresses", true); +local secret = assert(module:get_option_string("rest_credentials"), "rest_credentials is a required setting"); +local auth_type = assert(secret:match("^%S+"), "Format of rest_credentials MUST be like 'Bearer secret'"); +assert(auth_type == "Bearer", "Only 'Bearer' is supported in rest_credentials"); + +local jsonmap = module:require"jsonmap"; +-- Bearer token +local function check_credentials(request) + return request.headers.authorization == secret; +end + +local function parse(mimetype, data) + mimetype = mimetype and mimetype:match("^[^; ]*"); + if mimetype == "application/xmpp+xml" then + return xml.parse(data); + elseif mimetype == "application/json" then + local parsed, err = json.decode(data); + if not parsed then + return parsed, err; + end + return jsonmap.json2st(parsed); + elseif mimetype == "text/plain" then + return st.message({ type = "chat" }, data); + end + return nil, "unknown-payload-type"; +end + +local supported_types = { "application/xmpp+xml", "application/json" }; + +local function decide_type(accept) + -- assumes the accept header is sorted + local ret = supported_types[1]; + for i = 2, #supported_types do + if (accept:find(supported_types[i], 1, true) or 1000) < (accept:find(ret, 1, true) or 1000) then + ret = supported_types[i]; + end + end + return ret; +end + +local function encode(type, s) + if type == "application/json" then + return json.encode(jsonmap.st2json(s)); + elseif type == "text/plain" then + return s:get_child_text("body") or ""; + end + return tostring(s); +end + +local function handle_post(event) + local request, response = event.request, event.response; + if not request.headers.authorization then + response.headers.www_authenticate = ("%s realm=%q"):format(auth_type, module.host.."/"..module.name); + return 401; + elseif not check_credentials(request) then + return 401; + end + local payload, err = parse(request.headers.content_type, request.body); + if not payload then + -- parse fail + return errors.new({ code = 400, text = "Failed to parse payload" }, { error = err, type = request.headers.content_type, data = request.body }); + end + if payload.attr.xmlns then + return errors.new({ code = 422, text = "'xmlns' attribute must be empty" }); + elseif payload.name ~= "message" and payload.name ~= "presence" and payload.name ~= "iq" then + return errors.new({ code = 422, text = "Invalid stanza, must be 'message', 'presence' or 'iq'." }); + end + local to = jid.prep(payload.attr.to); + if not to then + return errors.new({ code = 422, text = "Invalid destination JID" }); + end + local from = module.host; + if allow_any_source and payload.attr.from then + from = jid.prep(payload.attr.from); + if not from then + return errors.new({ code = 422, text = "Invalid source JID" }); + end + if validate_from_addresses and not jid.compare(from, module.host) then + return errors.new({ code = 403, text = "Source JID must belong to current host" }); + end + end + payload.attr = { + from = from, + to = to, + id = payload.attr.id or id.medium(), + type = payload.attr.type, + ["xml:lang"] = payload.attr["xml:lang"], + }; + module:log("debug", "Received[rest]: %s", payload:top_tag()); + local send_type = decide_type((request.headers.accept or "") ..",".. request.headers.content_type) + if payload.name == "iq" then + if payload.attr.type ~= "get" and payload.attr.type ~= "set" then + return errors.new({ code = 422, text = "'iq' stanza must be of type 'get' or 'set'" }); + elseif #payload.tags ~= 1 then + return errors.new({ code = 422, text = "'iq' stanza must have exactly one child tag" }); + end + return module:send_iq(payload):next( + function (result) + module:log("debug", "Sending[rest]: %s", result.stanza:top_tag()); + response.headers.content_type = send_type; + return encode(send_type, result.stanza); + end, + function (error) + if error.context.stanza then + response.headers.content_type = send_type; + module:log("debug", "Sending[rest]: %s", error.context.stanza:top_tag()); + return encode(send_type, error.context.stanza); + else + return error; + end + end); + else + local origin = {}; + function origin.send(stanza) + module:log("debug", "Sending[rest]: %s", stanza:top_tag()); + response.headers.content_type = send_type; + response:send(encode(send_type, stanza)); + return true; + end + module:send(payload, origin); + return 202; + end +end + +-- Handle stanzas submitted via HTTP +module:depends("http"); +module:provides("http", { + route = { + POST = handle_post; + }; + }); + +-- Forward stanzas from XMPP to HTTP and return any reply +local rest_url = module:get_option_string("rest_callback_url", nil); +if rest_url then + local send_type = module:get_option_string("rest_callback_content_type", "application/xmpp+xml"); + if send_type == "json" then + send_type = "application/json"; + end + + module:set_status("info", "Not yet connected"); + http.request(rest_url, { + method = "OPTIONS", + }, function (body, code, response) + if code == 0 then + return module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body); + else + module:set_status("info", "Connected"); + end + if code == 200 and response.headers.accept then + send_type = decide_type(response.headers.accept); + module:log("debug", "Set 'rest_callback_content_type' = %q based on Accept header", send_type); + end + end); + + local code2err = { + [400] = { condition = "bad-request"; type = "modify" }; + [401] = { condition = "not-authorized"; type = "auth" }; + [402] = { condition = "not-authorized"; type = "auth" }; + [403] = { condition = "forbidden"; type = "auth" }; + [404] = { condition = "item-not-found"; type = "cancel" }; + [406] = { condition = "not-acceptable"; type = "modify" }; + [408] = { condition = "remote-server-timeout"; type = "wait" }; + [409] = { condition = "conflict"; type = "cancel" }; + [410] = { condition = "gone"; type = "cancel" }; + [411] = { condition = "bad-request"; type = "modify" }; + [412] = { condition = "bad-request"; type = "modify" }; + [413] = { condition = "resource-constraint"; type = "modify" }; + [414] = { condition = "resource-constraint"; type = "modify" }; + [415] = { condition = "bad-request"; type = "modify" }; + [429] = { condition = "resource-constraint"; type = "wait" }; + [431] = { condition = "resource-constraint"; type = "wait" }; + + [500] = { condition = "internal-server-error"; type = "cancel" }; + [501] = { condition = "feature-not-implemented"; type = "modify" }; + [502] = { condition = "remote-server-timeout"; type = "wait" }; + [503] = { condition = "service-unavailable"; type = "cancel" }; + [504] = { condition = "remote-server-timeout"; type = "wait" }; + [507] = { condition = "resource-constraint"; type = "wait" }; + }; + + local function handle_stanza(event) + local stanza, origin = event.stanza, event.origin; + local reply_needed = stanza.name == "iq"; + local receipt; + + if stanza.attr.type == "error" then + reply_needed = false; + end + + if stanza.name == "message" and stanza.attr.id and stanza:get_child("urn:xmpp:receipts", "request") then + reply_needed = true; + receipt = st.stanza("received", { xmlns = "urn:xmpp:receipts", id = stanza.id }); + end + + local request_body = encode(send_type, stanza); + + -- Keep only the top level element and let the rest be GC'd + stanza = st.clone(stanza, true); + + module:log("debug", "Sending[rest]: %s", stanza:top_tag()); + http.request(rest_url, { + body = request_body, + headers = { + ["Content-Type"] = send_type, + ["Content-Language"] = stanza.attr["xml:lang"], + Accept = table.concat(supported_types, ", "); + }, + }, function (body, code, response) + if code == 0 then + module:log_status("error", "Could not connect to callback URL %q: %s", rest_url, body); + origin.send(st.error_reply(stanza, "wait", "recipient-unavailable", body)); + return; + else + module:set_status("info", "Connected"); + end + local reply; + + if code == 202 or code == 204 then + if not reply_needed then + -- Delivered, no reply + return; + end + else + local parsed, err = parse(response.headers["content-type"], body); + if not parsed then + module:log("warn", "Failed parsing data from REST callback: %s, %q", err, body); + elseif parsed.name ~= stanza.name then + module:log("warn", "REST callback responded with the wrong stanza type, got %s but expected %s", parsed.name, stanza.name); + else + parsed.attr = { + from = stanza.attr.to, + to = stanza.attr.from, + id = parsed.attr.id or id.medium(); + type = parsed.attr.type, + ["xml:lang"] = parsed.attr["xml:lang"], + }; + if parsed.name == "message" and parsed.attr.type == "groupchat" then + parsed.attr.to = jid.bare(stanza.attr.from); + end + if not stanza.attr.type and parsed:get_child("error") then + parsed.attr.type = "error"; + end + if parsed.attr.type == "error" then + parsed.attr.id = stanza.attr.id; + elseif parsed.name == "iq" then + parsed.attr.id = stanza.attr.id; + parsed.attr.type = "result"; + end + reply = parsed; + end + end + + if not reply then + local code_hundreds = code - (code % 100); + if code_hundreds == 200 then + reply = st.reply(stanza); + if stanza.name ~= "iq" then + reply.attr.id = id.medium(); + end + -- TODO presence/status=body ? + elseif code2err[code] then + reply = st.error_reply(stanza, errors.new(code, nil, code2err)); + elseif code_hundreds == 400 then + reply = st.error_reply(stanza, "modify", "bad-request", body); + elseif code_hundreds == 500 then + reply = st.error_reply(stanza, "cancel", "internal-server-error", body); + else + reply = st.error_reply(stanza, "cancel", "undefined-condition", body); + end + end + + if receipt then + reply:add_direct_child(receipt); + end + + module:log("debug", "Received[rest]: %s", reply:top_tag()); + + origin.send(reply); + end); + + return true; + end + + if module:get_host_type() == "component" then + module:hook("iq/bare", handle_stanza, -1); + module:hook("message/bare", handle_stanza, -1); + module:hook("presence/bare", handle_stanza, -1); + module:hook("iq/full", handle_stanza, -1); + module:hook("message/full", handle_stanza, -1); + module:hook("presence/full", handle_stanza, -1); + module:hook("iq/host", handle_stanza, -1); + module:hook("message/host", handle_stanza, -1); + module:hook("presence/host", handle_stanza, -1); + else + -- Don't override everything on normal VirtualHosts + module:hook("iq/host", handle_stanza, -1); + module:hook("message/host", handle_stanza, -1); + module:hook("presence/host", handle_stanza, -1); + end +end + +local http_server = require "net.http.server"; +module:hook_object_event(http_server, "http-error", function (event) + local request, response = event.request, event.response; + if true or decide_type(request and request.headers.accept or "") == "application/json" then + if response then + response.headers.content_type = "application/json"; + end + return json.encode({ + type = "error", + error = event.error, + code = event.code, + }); + end +end, 10); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_rest/README.markdown 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_rest/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,380 @@ +--- +labels: +- 'Stage-Alpha' +summary: RESTful XMPP API +--- + +# Introduction + +This is yet another RESTful API for sending and receiving stanzas via +Prosody. It can be used to build bots and components implemented as HTTP +services. + +# Usage + +## Enabling + +``` {.lua} +Component "rest.example.net" "rest" +rest_credentials = "Bearer dmVyeSBzZWNyZXQgdG9rZW4K" +``` + +## Sending stanzas + +The API endpoint becomes available at the path `/rest`, so the full URL +will be something like `https://your-prosody.example:5281/rest`. + +To try it, simply `curl` an XML stanza payload: + +``` {.sh} +curl https://prosody.example:5281/rest \ + --oauth2-bearer dmVyeSBzZWNyZXQgdG9rZW4K \ + -H 'Content-Type: application/xmpp+xml' \ + --data-binary ' + Hello! + ' +``` + +or a JSON payload: + +``` {.sh} +curl https://prosody.example:5281/rest \ + --oauth2-bearer dmVyeSBzZWNyZXQgdG9rZW4K \ + -H 'Content-Type: application/json' \ + --data-binary '{ + "body" : "Hello!", + "kind" : "message", + "to" : "user@example.org", + "type" : "chat" + }' +``` + +The `Content-Type` header is important! + +### Replies + +A POST containing an `` stanza automatically wait for the reply, +long-polling style. + +``` {.sh} +curl https://prosody.example:5281/rest \ + --oauth2-bearer dmVyeSBzZWNyZXQgdG9rZW4K \ + -H 'Content-Type: application/xmpp+xml' \ + --data-binary ' + + ' +``` + +Replies to other kinds of stanzas that are generated by the same Prosody +instance *MAY* be returned in the HTTP response. Replies from other +entities (connected clients or remote servers) will not be returned, but +can be forwarded via the callback API described in the next section. + +## Receiving stanzas + +TL;DR: Set this webhook callback URL, get XML `POST`-ed there. + +``` {.lua} +Component "rest.example.net" "rest" +rest_credentials = "Bearer dmVyeSBzZWNyZXQgdG9rZW4K" +rest_callback_url = "http://my-api.example:9999/stanzas" +``` + +To enable JSON payloads set + +``` {.lua} +rest_callback_content_type = "application/json" +``` + +Example callback looks like: + +``` {.xml} +POST /stanzas HTTP/1.1 +Content-Type: application/xmpp+xml +Content-Length: 102 + + +Hello + +``` + +or as JSON: + +``` {.json} +POST /stanzas HTTP/1.1 +Content-Type: application/json +Content-Length: 133 + +{ + "body" : "Hello", + "from" : "user@example.com", + "kind" : "message", + "to" : "bot@rest.example.net", + "type" : "chat" +} +``` + +### Replying + +To accept the stanza without returning a reply, respond with HTTP status +code `202` or `204`. + +HTTP status codes in the `4xx` and `5xx` range are mapped to an +appropriate stanza error. + +For full control over the response, set the `Content-Type` header to +`application/xmpp+xml` and return an XMPP stanza as an XML snippet. + +``` {.xml} +HTTP/1.1 200 Ok +Content-Type: application/xmpp+xml + + +Yes, this is bot + +``` + +## Payload format + +### JSON + +``` {.json} +{ + "body" : "Hello!", + "kind" : "message", + "type" : "chat" +} +``` + +Further JSON object keys as follows: + +#### Messages + +`kind` +: `"message"` + +`type` +: Commonly `"chat"` for 1-to-1 messages and `"groupchat"` for group + chat messages. Others include `"normal"`, `"headline"` and + `"error"`. + +`body` +: Human-readable message text. + +`subject` +: Message subject or MUC topic. + +`html` +: HTML. + +`oob_url` +: URL of an out-of-band resource, often used for images. + +#### Presence + +`kind` +: `"presence"` + +`type` +: Empty for online or `"unavailable"` for offline. + +`show` +: [Online + status](https://xmpp.org/rfcs/rfc6121.html#presence-syntax-children-show), + `away`, `dnd` etc. + +`status` +: Human-readable status message. + +`join` +: Boolean. Join a group chat. + +#### Info-Queries + +Only one type of payload can be included in an `iq`. + +`kind` +: `"iq"` + +`type` +: `"get"` or `"set"` for queries, `"response"` or `"error"` for + replies. + +`ping` +: Send a ping. Get a pong. Maybe. + +`disco` +: Retrieve service discovery information about an entity. + +`items` +: Discover list of items (other services, groupchats etc). + +### XML + +``` {.xml} + +... + +``` + +An XML declaration (``) **MUST NOT** be included. + +The payload MUST contain one (1) `message`, `presence` or `iq` stanza. + +The stanzas MUST NOT have an `xmlns` attribute, and the default/empty +namespace is treated as `jabber:client`. + +# Examples + +## Python / Flask + +Simple echo bot that responds to messages as XML: + +``` {.python} +from flask import Flask, Response, request +import xml.etree.ElementTree as ET + +app = Flask("echobot") + + +@app.before_request +def parse(): + request.stanza = ET.fromstring(request.data) + + +@app.route("/", methods=["POST"]) +def hello(): + if request.stanza.tag == "message": + return Response( + "Yes this is bot", + content_type="application/xmpp+xml", + ) + + return Response(status=501) + + +if __name__ == "__main__": + app.run() +``` + +And a JSON variant: + +``` {.python} +from flask import Flask, Response, request, jsonify + +app = Flask("echobot") + + +@app.route("/", methods=["POST"]) +def hello(): + print(request.data) + if request.is_json: + data = request.get_json() + if data["kind"] == "message": + return jsonify({"body": "hello"}) + + return Response(status=501) + + +if __name__ == "__main__": + app.run() +``` + +Remember to set `rest_callback_content_type = "application/json"` for +this to work. + +# JSON mapping + +This section describes the JSON mapping. It can't represent any possible +stanza, for full flexibility use the XML mode. + +## Stanza basics + +`kind` +: String representing the kind of stanza, one of `"message"`, + `"presence"` or `"iq"`. + +`type` +: String with the type of stanza, appropriate values vary depending on + `kind`, see [RFC 6121]. E.g.`"chat"` for *message* stanzas etc. + +`to` +: String containing the XMPP Address of the destination / recipient of + the stanza. + +`from` +: String containing the XMPP Address of the sender the stanza. + +`id` +: String with a reasonably unique identifier for the stanza. + +## Basic Payloads + +### Messages + +`body` +: String, human readable text message. + +`subject` +: String, human readable summary equivalent to an email subject or the + chat room topic in a `type:groupchat` message. + +### Presence + +`show` +: String representing availability, e.g. `"away"`, `"dnd"`. No value + means a normal online status. See [RFC 6121] for the full list. + +`status` +: String with a human readable text message describing availability. + +## More payloads + +### Messages + +`state` +: String with current chat state, e.g. `"active"` (default) and + `"composing"` (typing). + +`html` +: String with HTML allowing rich formating. **MUST** be contained in a + `` element. + +`oob_url` +: String with an URL of an external resource. + +### Presence + +`join` +: Boolean, used to join group chats. + +### IQ + +`ping` +: Boolean, a simple ping query. "Pongs" have only basic fields + presents. + +`version` +: Map with `name`, `version` fields, and optionally an `os` field, to + describe the software. + +#### Service Discovery + +`disco` + +: Boolean `true` in a `kind:iq` `type:get` for a service discovery + query. + + Responses have a map containing an array of available features in + the `features` key and an array of "identities" in the `identities` + key. Each identity has a `category` and `type` field as well as an + optional `name` field. See [XEP-0030] for further details. + +`items` +: Boolean `true` in a `kind:iq` `type:get` for a service discovery + items list query. The response contain an array of items like + `{"jid":"xmpp.address.here","name":"Description of item"}`. + +# Compatibility + +Requires Prosody trunk / 0.12 diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_s2s_keepalive/mod_s2s_keepalive.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_s2s_keepalive/mod_s2s_keepalive.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_s2s_keepalive/mod_s2s_keepalive.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_s2s_keepalive/mod_s2s_keepalive.lua 2020-01-28 09:32:42.000000000 +0000 @@ -1,29 +1,85 @@ local st = require "util.stanza"; +local watchdog = require "util.watchdog"; local keepalive_servers = module:get_option_set("keepalive_servers"); local keepalive_interval = module:get_option_number("keepalive_interval", 60); +local keepalive_timeout = module:get_option_number("keepalive_timeout", 593); local host = module.host; +local s2sout = prosody.hosts[host].s2sout; local function send_pings() - for remote_domain, session in pairs(hosts[host].s2sout) do - if session.type == "s2sout" -- as opposed to _unauthed + local ping_hosts = {}; + + for remote_domain, session in pairs(s2sout) do + if session.type ~= "s2sout_unauthed" and (not(keepalive_servers) or keepalive_servers:contains(remote_domain)) then session.sends2s(st.iq({ to = remote_domain, type = "get", from = host, id = "keepalive" }) :tag("ping", { xmlns = "urn:xmpp:ping" }) ); - -- Note: We don't actually check if this comes back. end end for session in pairs(prosody.incoming_s2s) do - if session.type == "s2sin" -- as opposed to _unauthed + if session.type ~= "s2sin_unauthed" + and session.to_host == host and (not(keepalive_servers) or keepalive_servers:contains(session.from_host)) then + if not s2sout[session.from_host] then ping_hosts[session.from_host] = true; end session.sends2s " "; -- If the connection is dead, this should make it time out. end end + + -- ping remotes we only have s2sin from + for remote_domain in pairs(ping_hosts) do + module:send(st.iq({ to = remote_domain, type = "get", from = host, id = "keepalive" }) + :tag("ping", { xmlns = "urn:xmpp:ping" }) + ); + end + return keepalive_interval; end +module:hook("s2sin-established", function (event) + local session = event.session; + if session.watchdog_keepalive then return end -- in case mod_bidi fires this twice + if keepalive_servers and not keepalive_servers:contains(session.from_host) then return end + session.watchdog_keepalive = watchdog.new(keepalive_timeout, function () + session.log("info", "Keepalive ping timed out, closing connection"); + session:close("connection-timeout"); + end); +end); + +module:hook("s2sout-established", function (event) + local session = event.session; + if session.watchdog_keepalive then return end -- in case mod_bidi fires this twice + if keepalive_servers and not keepalive_servers:contains(session.from_host) then return end + session.watchdog_keepalive = watchdog.new(keepalive_timeout, function () + session.log("info", "Keepalive ping timed out, closing connection"); + session:close("connection-timeout"); + end); +end); + +module:hook("iq-result/host/keepalive", function (event) + local origin = event.origin; + if origin.watchdog_keepalive then + origin.watchdog_keepalive:reset(); + end + if s2sout[origin.from_host] and s2sout[origin.from_host].watchdog_keepalive then + s2sout[origin.from_host].watchdog_keepalive:reset(); + end + return true; +end); + +module:hook("iq-error/host/keepalive", function (event) + local origin = event.origin; + if origin.dummy then return end -- Probably a sendq bounce + + if origin.type == "s2sin" or origin.type == "s2sout" then + -- An error from the remote means connectivity is ok, + -- so treat it the same as a result + return module:fire_event("iq-result/host/keepalive", event); + end +end); + module:add_timer(keepalive_interval, send_pings); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_s2s_keepalive/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_s2s_keepalive/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_s2s_keepalive/README.markdown 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_s2s_keepalive/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -17,6 +17,9 @@ `keepalive_servers`. The ping interval can be set using `keepalive_interval`. +If no response to the ping has been received in about 10 minutes (or +`keepalive_timeout` seconds) the s2s connections are closed. + ``` lua modules_enabled = { ... @@ -24,13 +27,15 @@ } keepalive_servers = { "conference.prosody.im"; "rooms.swift.im" } -keepalive_interval = "300" -- (in seconds, default is 60 ) +keepalive_interval = 90 -- (in seconds, default is 60 ) +keepalive_timeout = 300 -- (in seconds, default is 593 ) ``` Compatibility ============= ------- ----------------------- + 0.11 Works 0.10 Works 0.9 Works ------- ----------------------- diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_smacks/mod_smacks.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_smacks/mod_smacks.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_smacks/mod_smacks.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_smacks/mod_smacks.lua 2020-01-28 09:32:42.000000000 +0000 @@ -5,7 +5,7 @@ -- Copyright (C) 2012-2015 Kim Alvefur -- Copyright (C) 2012 Thijs Alkemade -- Copyright (C) 2014 Florian Zeitz --- Copyright (C) 2016-2019 Thilo Molitor +-- Copyright (C) 2016-2020 Thilo Molitor -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -558,12 +558,14 @@ -- 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 + -- We have to use the send of "session" because we don't want to add our resent stanzas + -- to the outgoing queue again local queue = original_session.outgoing_stanza_queue; - original_session.log("debug", "#queue = %d", #queue); + session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", #queue); for i=1,#queue do - original_session.send(queue[i]); + session.send(queue[i]); end - original_session.log("debug", "#queue = %d -- after send", #queue); + session.log("debug", "all stanzas resent, now disabling send() in this session, #queue = %d", #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; diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_stats39/mod_stats39.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_stats39/mod_stats39.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_stats39/mod_stats39.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_stats39/mod_stats39.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,18 @@ +local statsman = require "core.statsmanager"; +local st = require "util.stanza"; +local s_format = string.format; + +module:add_feature("http://jabber.org/protocol/stats"); + +module:hook("iq/host/http://jabber.org/protocol/stats:query", function (event) + local origin, stanza = event.origin, event.stanza; + local stats, _, extra = statsman.get_stats(); + local reply = st.reply(stanza); + reply:tag("query", { xmlns = "http://jabber.org/protocol/stats" }); + for stat, value in pairs(stats) do + local unit = extra[stat] and extra[stat].units; + reply:tag("stat", { name = stat, unit = unit, value = s_format("%.12g", value) }):up(); + end + origin.send(reply); + return true; +end) diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_stats39/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_stats39/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_stats39/README.markdown 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_stats39/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,58 @@ +This module provides **public** access to Prosodys +[internal statistics][doc:statistics] trough the +[XEP-0039: Statistics Gathering] protocol. This is a simple protocol +that returns triplets of name, unit and value for each know statistic +collected by Prosody. The names used are the internal names assigned by +modules or statsmanager, names from the registry are **not** used. + +# Configuration + +Enabled as usual by adding to [`modules_enabled`][doc:modules_enabled]: + +```lua +-- Enable Prosodys internal statistics gathering +statistics = "internal" + +-- and enable the module +modules_enabled = { + -- other modules + "stats39"; +} +``` + +# Usage + + +## Example + +Statistics can be queried from the XML console of clients that have one: + +```xml +C: + + + + +S: + + + + + + + + + + + + + + + + + + + + +``` + diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_storage_xmlarchive/mod_storage_xmlarchive.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_storage_xmlarchive/mod_storage_xmlarchive.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_storage_xmlarchive/mod_storage_xmlarchive.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_storage_xmlarchive/mod_storage_xmlarchive.lua 2020-01-28 09:32:42.000000000 +0000 @@ -36,6 +36,9 @@ local day = dt.date(when); local ok, err = dm.append_raw(username.."@"..day, self.host, self.store, "xml", data); if not ok then + -- append_raw, unlike list_append, does not log anything on failure atm, so we do so here + module:log("error", "Unable to write to %s storage ('%s') for user: %s@%s", + self.store, err, username, module.host) return nil, err; end diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_turncredentials/mod_turncredentials.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_turncredentials/mod_turncredentials.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_turncredentials/mod_turncredentials.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_turncredentials/mod_turncredentials.lua 2020-01-28 09:32:42.000000000 +0000 @@ -23,8 +23,8 @@ if origin.type ~= "c2s" then return; end - local now = os_time() + ttl; - local userpart = tostring(now); + local expires_at = os_time() + ttl; + local userpart = tostring(expires_at); local nonce = base64.encode(hmac_sha1(secret, tostring(userpart), false)); origin.send(st.reply(stanza):tag("services", {xmlns = "urn:xmpp:extdisco:1"}) :tag("service", { type = "stun", host = host, port = ("%d"):format(port) }):up() @@ -40,12 +40,12 @@ if origin.type ~= "c2s" then return; end - local now = os_time() + ttl; - local userpart = tostring(now); + local expires_at = os_time() + ttl; + local userpart = tostring(expires_at); local nonce = base64.encode(hmac_sha1(secret, tostring(userpart), false)); origin.send(st.reply(stanza):tag("services", {xmlns = "urn:xmpp:extdisco:2"}) :tag("service", { type = "stun", host = host, port = ("%d"):format(port) }):up() - :tag("service", { type = "turn", host = host, port = ("%d"):format(port), username = userpart, password = nonce, expires = datetime(ttl), restricted = "1" }):up() + :tag("service", { type = "turn", host = host, port = ("%d"):format(port), username = userpart, password = nonce, expires = datetime(expires_at), restricted = "1" }):up() ); return true; end); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_warn_legacy_tls/mod_warn_legacy_tls.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_warn_legacy_tls/mod_warn_legacy_tls.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_warn_legacy_tls/mod_warn_legacy_tls.lua 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_warn_legacy_tls/mod_warn_legacy_tls.lua 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,26 @@ +local st = require"util.stanza"; +local host = module.host; + +local deprecated_protocols = module:get_option_set("legacy_tls_versions", { "SSLv3", "TLSv1", "TLSv1.1" }); +local warning_message = module:get_option_string("legacy_tls_warning", "Your connection is encrypted using the %s protocol, which has known problems and will be disabled soon. Please upgrade your client."); + +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 ok, protocol = pcall(function(session) + return session.conn:socket():info"protocol"; + end, session); + if not ok then + module:log("debug", "Could not determine TLS version: %s", protocol); + elseif deprecated_protocols:contains(protocol) then + session.log("warn", "Uses %s", protocol); + module:add_timer(15, function () + if session.type == "c2s" and session.resource then + session.send(st.message({ from = host, type = "headline", to = session.full_jid }, warning_message:format(protocol))); + end + end); + else + module:log("debug", "Using acceptable TLS version: %s", protocol); + end +end); diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_warn_legacy_tls/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_warn_legacy_tls/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_warn_legacy_tls/README.markdown 1970-01-01 00:00:00.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_warn_legacy_tls/README.markdown 2020-01-28 09:32:42.000000000 +0000 @@ -0,0 +1,28 @@ +TLS 1.0 and TLS 1.1 are about to be obsolete. This module warns clients +if they are using those versions, to prepare for disabling them. + +# Configuration + +``` {.lua} +modules_enabled = { + -- other modules etc + "warn_legacy_tls"; +} + +-- This is the default, you can leave it out if you don't wish to +-- customise or translate the message sent. +-- '%s' will be replaced with the TLS version in use. +legacy_tls_warning = [[ +Your connection is encrypted using the %s protocol, which has been demonstrated to be insecure and will be disabled soon. Please upgrade your client. +]] +``` + +## Options + +`legacy_tls_warning` +: A string. The text of the message sent to clients that use outdated + TLS versions. Default as in the above example. + +`legacy_tls_versions` +: Set of TLS versions, defaults to + `{ "SSLv3", "TLSv1", "TLSv1.1" }`{.lua}, i.e. TLS \< 1.2. diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_web_push/mod_web_push.lua prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_web_push/mod_web_push.lua --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_web_push/mod_web_push.lua 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_web_push/mod_web_push.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,404 +0,0 @@ --- XEP-XXXX: Web Push (aka: My mobile OS vendor won't let me have persistent TCP connections, take two) --- Copyright (C) 2019 Maxime “pep” Buquet --- --- Heavily based on mod_cloud_notify. --- Copyright (C) 2015-2016 Kim Alvefur --- Copyright (C) 2017-2018 Thilo Molitor - - -local st = require"util.stanza"; -local dataform = require "util.dataforms"; -local http = require "net.http"; - -local os_time = os.time; -local next = next; -local jid = require"util.jid"; -local filters = require"util.filters"; - -local xmlns_webpush = "urn:xmpp:webpush:0"; - -local max_push_devices = module:get_option_number("push_max_devices", 5); -local dummy_body = module:get_option_string("push_notification_important_body", "New Message!"); - -local host_sessions = prosody.hosts[module.host].sessions; - --- TODO: Generate it at setup time. Obviously not to be used other than for --- testing purposes, or at all. --- ECDH keypair -local server_pubkey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhxZpb8yIVc/2hNesGLGAxEakyYy0MqEetjgL7BIOm8ybhVKxapKqNXjXJ+NOO5/b0Z0UuBg/HynGnf0xKKNhBQ=="; -local server_privkey = "MHcCAQEEIPhZac9pQ8aVTx9a5JyRcqfk3nuQQUFy3PaDcSWleojzoAoGCCqGSM49AwEHoUQDQgAEhxZpb8yIVc/2hNesGLGAxEakyYy0MqEetjgL7BIOm8ybhVKxapKqNXjXJ+NOO5/b0Z0UuBg/HynGnf0xKKNhBQ=="; - --- Advertize disco feature -local function account_disco_info(event) - local form = dataform.new { - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/webpush#public-key" }; - { name = "webpush#public-key", value = server_pubkey }; - }; - (event.reply or event.stanza):tag("feature", {var=xmlns_webpush}):up() - :add_child(form:form({}, "result")); -end -module:hook("account-disco-info", account_disco_info); - --- ordered table iterator, allow to iterate on the natural order of the keys of a table, --- see http://lua-users.org/wiki/SortedIteration -local function __genOrderedIndex( t ) - local orderedIndex = {} - for key in pairs(t) do - table.insert( orderedIndex, key ) - end - -- sort in reverse order (newest one first) - table.sort( orderedIndex, function(a, b) - if a == nil or t[a] == nil or b == nil or t[b] == nil then return false end - -- only one timestamp given, this is the newer one - if t[a].timestamp ~= nil and t[b].timestamp == nil then return true end - if t[a].timestamp == nil and t[b].timestamp ~= nil then return false end - -- both timestamps given, sort normally - if t[a].timestamp ~= nil and t[b].timestamp ~= nil then return t[a].timestamp > t[b].timestamp end - return false -- normally not reached - end) - return orderedIndex -end -local function orderedNext(t, state) - -- Equivalent of the next function, but returns the keys in timestamp - -- order. We use a temporary ordered key table that is stored in the - -- table being iterated. - - local key = nil - --print("orderedNext: state = "..tostring(state) ) - if state == nil then - -- the first time, generate the index - t.__orderedIndex = __genOrderedIndex( t ) - key = t.__orderedIndex[1] - else - -- fetch the next value - for i = 1, #t.__orderedIndex do - if t.__orderedIndex[i] == state then - key = t.__orderedIndex[i+1] - end - end - end - - if key then - return key, t[key] - end - - -- no more value to return, cleanup - t.__orderedIndex = nil - return -end -local function orderedPairs(t) - -- Equivalent of the pairs() function on tables. Allows to iterate - -- in order - return orderedNext, t, nil -end - --- small helper function to return new table with only "maximum" elements containing only the newest entries -local function reduce_table(table, maximum) - local count = 0; - local result = {}; - for key, value in orderedPairs(table) do - count = count + 1; - if count > maximum then break end - result[key] = value; - end - return result; -end - -local push_store = (function() - local store = module:open_store(); - local push_services = {}; - local api = {}; - function api:get(user) - if not push_services[user] then - local err; - push_services[user], err = store:get(user); - if not push_services[user] and err then - module:log("warn", "Error reading web push notification storage for user '%s': %s", user, tostring(err)); - push_services[user] = {}; - return push_services[user], false; - end - end - if not push_services[user] then push_services[user] = {} end - return push_services[user], true; - end - function api:set(user, data) - push_services[user] = reduce_table(data, max_push_devices); - local ok, err = store:set(user, push_services[user]); - if not ok then - module:log("error", "Error writing web push notification storage for user '%s': %s", user, tostring(err)); - return false; - end - return true; - end - function api:set_identifier(user, push_identifier, data) - local services = self:get(user); - services[push_identifier] = data; - return self:set(user, services); - end - return api; -end)(); - -local function push_enable(event) - local origin, stanza = event.origin, event.stanza; - local enable = stanza.tags[1]; - origin.log("debug", "Attempting to enable web push notifications"); - -- MUST contain a 'href' attribute of the XMPP Push Service being enabled - local push_endpoint = nil; - local push_auth = nil; - local push_p256dh = nil; - - local endpoint_tag = enable:get_child('endpoint'); - if endpoint_tag ~= nil then - push_endpoint = endpoint_tag:get_text(); - end - local auth_tag = enable:get_child('auth'); - if auth_tag ~= nil then - push_auth = auth_tag:get_text(); - end - local p256dh_tag = enable:get_child('p256dh'); - if p256dh_tag ~= nil then - push_p256dh = p256dh_tag:get_text(); - end - if not push_endpoint or not push_auth or not push_p256dh then - origin.log("debug", "Web Push notification enable request missing 'endpoint', 'auth', or 'p256dh' tags"); - origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing enable child tag")); - return true; - end - local push_identifier = "foo"; - local push_service = push_endpoint; - local ok = push_store:set_identifier(origin.username, push_identifier, push_service); - if not ok then - origin.send(st.error_reply(stanza, "wait", "internal-server-error")); - else - origin.push_identifier = push_identifier; - origin.push_settings = push_service; - origin.log("info", "Web Push notifications enabled for %s (%s)", tostring(stanza.attr.from), tostring(origin.push_identifier)); - origin.send(st.reply(stanza)); - end - return true; -end -module:hook("iq-set/self/"..xmlns_webpush..":enable", push_enable); - --- module:hook("iq-set/self/"..xmlns_webpush..":disable", push_disable); - --- small helper function to extract relevant push settings -local function get_push_settings(stanza, session) - local to = stanza.attr.to; - local node = to and jid.split(to) or session.username; - local user_push_services = push_store:get(node); - return node, user_push_services; -end - -local function log_http_req(response_body, response_code, response) - module:log("debug", "FOO: response_body: %s; response_code: %s; response: %s", response_body, tostring(response_code), tostring(response)); -end - -local function handle_notify_request(stanza, node, user_push_services, log_push_decline) - local pushes = 0; - if not user_push_services or next(user_push_services) == nil then return pushes end - - for push_identifier, push_info in pairs(user_push_services) do - local send_push = true; -- only send push to this node when not already done for this stanza or if no stanza is given at all - if stanza then - if not stanza._push_notify then stanza._push_notify = {}; end - if stanza._push_notify[push_identifier] then - if log_push_decline then - module:log("debug", "Already sent push notification for %s@%s to %s", node, module.host, tostring(push_info)); - end - send_push = false; - end - stanza._push_notify[push_identifier] = true; - end - - if send_push then - local headers = { TTL = "60" }; - http.request(push_info, { method = "POST", headers = headers }, log_http_req); - pushes = pushes + 1; - end - end - return pushes; -end - --- publish on offline message -module:hook("message/offline/handle", function(event) - local node, user_push_services = get_push_settings(event.stanza, event.origin); - module:log("debug", "Invoking web push handle_notify_request() for offline stanza"); - handle_notify_request(event.stanza, node, user_push_services, true); -end, 1); - --- is this push a high priority one (this is needed for ios apps not using voip pushes) -local function is_important(stanza) - local st_name = stanza and stanza.name or nil; - if not st_name then return false; end -- nonzas are never important here - if st_name == "presence" then - return false; -- same for presences - elseif st_name == "message" then - -- unpack carbon copies - local stanza_direction = "in"; - local carbon; - local st_type; - -- support carbon copied message stanzas having an arbitrary message-namespace or no message-namespace at all - if not carbon then carbon = find(stanza, "{urn:xmpp:carbons:2}/forwarded/message"); end - if not carbon then carbon = find(stanza, "{urn:xmpp:carbons:1}/forwarded/message"); end - stanza_direction = carbon and stanza:child_with_name("sent") and "out" or "in"; - if carbon then stanza = carbon; end - st_type = stanza.attr.type; - - -- headline message are always not important - if st_type == "headline" then return false; end - - -- carbon copied outgoing messages are not important - if carbon and stanza_direction == "out" then return false; end - - -- We can't check for body contents in encrypted messages, so let's treat them as important - -- Some clients don't even set a body or an empty body for encrypted messages - - -- check omemo https://xmpp.org/extensions/inbox/omemo.html - if stanza:get_child("encrypted", "eu.siacs.conversations.axolotl") or stanza:get_child("encrypted", "urn:xmpp:omemo:0") then return true; end - - -- check xep27 pgp https://xmpp.org/extensions/xep-0027.html - if stanza:get_child("x", "jabber:x:encrypted") then return true; end - - -- check xep373 pgp (OX) https://xmpp.org/extensions/xep-0373.html - if stanza:get_child("openpgp", "urn:xmpp:openpgp:0") then return true; end - - local body = stanza:get_child_text("body"); - if st_type == "groupchat" and stanza:get_child_text("subject") then return false; end -- groupchat subjects are not important here - return body ~= nil and body ~= ""; -- empty bodies are not important - end - return false; -- this stanza wasn't one of the above cases --> it is not important, too -end - --- publish on unacked smacks message -local function process_smacks_stanza(stanza, session) - if session.push_identifier then - session.log("debug", "Invoking web push handle_notify_request() for smacks queued stanza"); - local user_push_services = {[session.push_identifier] = session.push_settings}; - local node = get_push_settings(stanza, session); - if handle_notify_request(stanza, node, user_push_services, true) ~= 0 then - if session.hibernating and not session.first_hibernated_push then - -- if important stanzas are treated differently (pushed with last-message-body field set to dummy string) - -- and the message was important (e.g. had a last-message-body field) OR if we treat all pushes equally, - -- then record the time of first push in the session for the smack module which will extend its hibernation - -- timeout based on the value of session.first_hibernated_push - if not dummy_body or (dummy_body and is_important(stanza)) then - session.first_hibernated_push = os_time(); - end - end - end - end - return stanza; -end - -local function process_smacks_queue(queue, session) - if not session.push_identifier then return; end - local user_push_services = {[session.push_identifier] = session.push_settings}; - local notified = { unimportant = false; important = false } - for i=1, #queue do - local stanza = queue[i]; - local node = get_push_settings(stanza, session); - local stanza_type = "unimportant" - if dummy_body and is_important(stanza) then stanza_type = "important"; end - if not notified[stanza_type] then -- only notify if we didn't try to push for this stanza type already - -- session.log("debug", "Invoking cloud handle_notify_request() for smacks queued stanza: %d", i); - if handle_notify_request(stanza, node, user_push_services, false) ~= 0 then - if session.hibernating and not session.first_hibernated_push then - -- if important stanzas are treated differently (pushed with last-message-body field set to dummy string) - -- and the message was important (e.g. had a last-message-body field) OR if we treat all pushes equally, - -- then record the time of first push in the session for the smack module which will extend its hibernation - -- timeout based on the value of session.first_hibernated_push - if not dummy_body or (dummy_body and is_important(stanza)) then - session.first_hibernated_push = os_time(); - end - end - session.log("debug", "Web Push handle_notify_request() > 0, not notifying for other queued stanzas of type %s", stanza_type); - notified[stanza_type] = true - end - end - end -end - --- smacks hibernation is started -local function hibernate_session(event) - local session = event.origin; - local queue = event.queue; - session.first_hibernated_push = nil; - -- process unacked stanzas - process_smacks_queue(queue, session); - -- process future unacked (hibernated) stanzas - filters.add_filter(session, "stanzas/out", process_smacks_stanza, -990); -end - --- smacks hibernation is ended -local function restore_session(event) - local session = event.resumed; - if session then -- older smacks module versions send only the "intermediate" session in event.session and no session.resumed one - filters.remove_filter(session, "stanzas/out", process_smacks_stanza); - session.first_hibernated_push = nil; - end -end - --- smacks ack is delayed -local function ack_delayed(event) - local session = event.origin; - local queue = event.queue; - -- process unacked stanzas (handle_notify_request() will only send push requests for new stanzas) - process_smacks_queue(queue, session); -end - --- archive message added -local function archive_message_added(event) - -- event is: { origin = origin, stanza = stanza, for_user = store_user, id = id } - -- only notify for new mam messages when at least one device is online - if not event.for_user or not host_sessions[event.for_user] then return; end - local stanza = event.stanza; - local user_session = host_sessions[event.for_user].sessions; - local to = stanza.attr.to; - to = to and jid.split(to) or event.origin.username; - - -- only notify if the stanza destination is the mam user we store it for - if event.for_user == to then - local user_push_services = push_store:get(to); - if next(user_push_services) == nil then return end - - -- only notify nodes with no active sessions (smacks is counted as active and handled separate) - local notify_push_services = {}; - for identifier, push_info in pairs(user_push_services) do - local identifier_found = nil; - for _, session in pairs(user_session) do - -- module:log("debug", "searching for '%s': identifier '%s' for session %s", tostring(identifier), tostring(session.push_identifier), tostring(session.full_jid)); - if session.push_identifier == identifier then - identifier_found = session; - break; - end - end - if identifier_found then - identifier_found.log("debug", "Not web push notifying '%s' of new MAM stanza (session still alive)", identifier); - else - notify_push_services[identifier] = push_info; - end - end - - handle_notify_request(event.stanza, to, notify_push_services, true); - end -end - -module:hook("smacks-hibernation-start", hibernate_session); -module:hook("smacks-hibernation-end", restore_session); -module:hook("smacks-ack-delayed", ack_delayed); -module:hook("archive-message-added", archive_message_added); - -function module.command(arg) - print("TODO: Generate server keypair") -end - -module:log("info", "Module loaded"); -function module.unload() - if module.unhook then - module:unhook("account-disco-info", account_disco_info); - module:unhook("iq-set/self/"..xmlns_webpush..":enable", push_enable); - -- module:unhook("iq-set/self/"..xmlns_webpush..":disable", push_disable); - end - - module:log("info", "Module unloaded"); -end diff -Nru prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_web_push/README.markdown prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_web_push/README.markdown --- prosody-modules-0.0~hg20191101.19e43b7a969d+dfsg/mod_web_push/README.markdown 2019-11-02 18:48:27.000000000 +0000 +++ prosody-modules-0.0~hg20200128.09e7e880e056+dfsg/mod_web_push/README.markdown 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ ---- -labels: -- 'Stage-Alpha' -summary: 'XEP-XXXX: Web Push' ---- - -Introduction -============ - -::: {.alert .alert-danger} -**This module is terribly untested and will only work with Firefox as it's -missing payload encryption. Other vendors require it all the time. Public and -private keys are also statically set in it.** -::: - -This is an implementation of the server bits of [XEP-XXXX: Web Push]. - -It allows web clients to register a "push server" which is notified about new -messages while the user is offline, disconnected or the session is hibernated -by [mod_smacks]. - -Push servers are provided by browser vendors. - -This module is heavily based on [mod_cloud_notify]. - -Details -======= - -[Push API](https://w3c.github.io/push-api/) is a specification by the W3C that -is essentially the same principle as Mobile OS vendors' Push notification -systems. It is implemented by most browsers vendors except Safari on iOS -(mobile). - -For more information, see: - -- https://developer.mozilla.org/en-US/docs/Web/API/Push_API -- https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications - -Compared to [XEP-0357: Push Notifications], Web Push doesn't need an App -Server. - -The general flow for subscription is: - -- XMPP server generate ECDH keypair, publishes public key -- XMPP client generates an ECDH keypair -- XMPP client fetches server public key -- XMPP client subscribes to browser Push server using the Web Push API, and - gets back an HTTP endpoint -- XMPP client enables Push notifications telling the server the HTTP endpoint, - and its public key - -The flow for notifications is as follow: - -- XMPP server receives an _important_[^1] message -- XMPP server generates something something JWT + signature with ECDH key -- XMPP server can optionally include payload encrypted for the client -- XMPP server initiates HTTP POST request to the Push server -- Push server sends notification to web browser - -Configuration -============= - - Option Default Description - ------------------------------------ ----------------- ------------------------------------------------------------------------------------------------------------------- - `push_notification_important_body` `New Message!` The body text to use when the stanza is important (see above), no message body is sent if this is empty - `push_max_devices` `5` The number of allowed devices per user (the oldest devices are automatically removed if this threshold is reached) - -There are privacy implications for enabling these options because -plaintext content and metadata will be shared with centralized servers -(the pubsub node) run by arbitrary app developers. - -Installation -============ - -Same as any other module. - -Configuration -============= - -Configured in-band by supporting clients. - -[^1]: As defined in mod_cloud_notify, or mod_csi_simple.