# HG changeset patch # User JC Brand # Date 1522140685 -7200 # Node ID 0f813e22e3fa969e6ff12ab94ec7ed6c0a1824b9 # Parent d0ca211e1b0e5271bf1dd60f846e422a7f9c037f# Parent 6140f9a030941a4497aa63a1dcbbc674ac43383c Merge commit diff -r d0ca211e1b0e -r 0f813e22e3fa mod_checkcerts/mod_checkcerts.lua --- a/mod_checkcerts/mod_checkcerts.lua Tue Mar 27 10:48:04 2018 +0200 +++ b/mod_checkcerts/mod_checkcerts.lua Tue Mar 27 10:51:25 2018 +0200 @@ -1,3 +1,4 @@ +local config = require "core.configmanager"; local ssl = require"ssl"; local datetime_parse = require"util.datetime".parse; local load_cert = ssl.loadcertificate; @@ -45,20 +46,20 @@ ssl_config = config.get("*", "ssl"); end if not ssl_config or not ssl_config.certificate then - log("warn", "Could not find a certificate to check"); + module:log("warn", "Could not find a certificate to check"); return; end local certfile = ssl_config.certificate; local fh, ferr = io.open(certfile); -- Load the file. if not fh then - log("warn", "Could not open certificate %s", ferr); + module:log("warn", "Could not open certificate %s", ferr); return; end local cert, lerr = load_cert(fh:read("*a")); -- And parse fh:close(); if not cert then - log("warn", "Could not parse certificate %s: %s", certfile, lerr or ""); + module:log("warn", "Could not parse certificate %s: %s", certfile, lerr or ""); return; end diff -r d0ca211e1b0e -r 0f813e22e3fa mod_component_http/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_component_http/README.markdown Tue Mar 27 10:51:25 2018 +0200 @@ -0,0 +1,108 @@ +--- +summary: 'Allows implementing a component or bot over HTTP' +... + +Introduction +============ + +This module allows you to implement a component that speaks HTTP. Stanzas (such as messages) coming from XMPP are sent to +a configurable URL as a HTTP POST. If the POST returns a response, that response is returned to the sender over XMPP. + +See also mod_post_msg. + +Example usage +------------- + +Example echo bot in PHP: + +``` php +body; + +// Send response +header('Content-Type: application/json'); +echo json_encode(array( + 'body' => "Did you say $received?" +)); + +?> +``` + +Configuration +============= + +The module is quite flexible, but should generally be loaded as a component like this: + +``` +Component "yourservice.example.com" "component_http" + component_post_url = "https://example.com/your-api" +``` + +Such a component would handle traffic for all JIDs with 'yourservice.example.com' as the hostname, such +as 'foobar@yourservice.example.com'. Although this example uses a subdomain, there is no requirement for +the component to use a subdomain. + +Available configuration options are: + + + Option Description + ------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------- + component\_post\_url The URL that will handle incoming stanzas + component\_post\_stanzas A list of stanza types to forward over HTTP. Defaults to `{ "message" }`. + +Details +======= + +Requests +-------- + +Each received stanza is converted into a JSON object, and submitted to `component_post_url` using a HTTP POST request. + +The JSON object always has the following properties: + + Property Description + -------------------------- ------------ + to The JID that the stanza was sent to (e.g. foobar@your.component.domain) + from The sender's JID. + kind The kind of stanza (will always be "message", "presence" or "iq". + stanza The full XML of the stanza. + +Additionally, the JSON object may contain the following properties: + + Property Description + -------------------------- ------------ + body If the stanza is a message, and it contains a body, this is the string content of the body. + + +Responses +--------- + +If you wish to respond to a stanza, you may include a reply when you respond to the HTTP request. + +Responses must have a HTTP status 200 (OK), and must set the Conent-Type header to `application/json`. + +A response may contain any of the properties of a request. If not supplied, then defaults are chosen. + +If 'to' and 'from' are not specified in the response, they are automatically swapped so that the reply is sent to the original sender of the stanza. + +If 'kind' is not set, it defaults to 'message', and if 'body' is set, this is automatically added as a message body. + +If 'stanza' is set, it overrides all of the above, and the supplied stanza is sent as-is using Prosody's normal routing rules. Note that stanzas +sent by components must have a 'to' and 'from'. + +Presence +-------- + +By default the module automatically handles presence to provide an always-on component, that automatically accepts subscription requests. + +This means that by default presence stanzas are not forwarded to the configured URL. To provide your own presence handling, you can override +this by adding "presence" to the component\_post\_stanzas option in your config. + + +Compatibility +============= + +Should work with all versions of Prosody from 0.9 upwards. diff -r d0ca211e1b0e -r 0f813e22e3fa mod_component_http/mod_component_http.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_component_http/mod_component_http.lua Tue Mar 27 10:51:25 2018 +0200 @@ -0,0 +1,102 @@ +local http = require "net.http"; +local json = require "util.json"; +local st = require "util.stanza"; +local xml = require "util.xml"; +local unpack = rawget(_G, "unpack") or table.unpack; + +local url = module:get_option_string("component_post_url"); +assert(url, "Missing required config option 'component_post_url'"); + +local stanza_kinds = module:get_option_set("post_stanza_types", { "message" }); + +local http_error_map = { + [0] = { "cancel", "remote-server-timeout", "Connection failure" }; + -- 4xx + [400] = { "modify", "bad-request" }; + [401] = { "auth", "not-authorized" }; + [402] = { "auth", "forbidden", "Payment required" }; + [403] = { "auth", "forbidden" }; + [404] = { "cancel", "item-not-found" }; + [410] = { "cancel", "gone" }; + -- 5xx + [500] = { "cancel", "internal-server-error" }; + [501] = { "cancel", "feature-not-implemented" }; + [502] = { "cancel", "remote-server-timeout", "Bad gateway" }; + [503] = { "wait", "remote-server-timeout", "Service temporarily unavailable" }; + [504] = { "wait", "remote-server-timeout", "Gateway timeout" }; +} + +local function error_reply(stanza, code) + local error = http_error_map[code] or { "cancel", "service-unavailable" }; + return st.error_reply(stanza, unpack(error, 1, 3)); +end + +function handle_stanza(event) + local origin, stanza = event.origin, event.stanza; + local request_body = json.encode({ + to = stanza.attr.to; + from = stanza.attr.from; + kind = stanza.name; + body = stanza.name == "message" and stanza:get_child_text("body") or nil; + stanza = tostring(stanza); + }); + http.request(url, { + body = request_body; + }, function (response_text, code, _, response) + if stanza.attr.type == "error" then return; end -- Avoid error loops, don't reply to error stanzas + if code == 200 and response_text and response.headers["content-type"] == "application/json" then + local response_data = json.decode(response_text); + if response_data.stanza then + local reply_stanza = xml.parse(response_data.stanza); + if reply_stanza then + reply_stanza.attr.from, reply_stanza.attr.to = stanza.attr.to, stanza.attr.from; + return origin.send(reply_stanza); + else + module:log("warn", "Unable to parse reply stanza"); + end + else + local stanza_kind = response_data.kind or "message"; + local to = response_data.to or stanza.attr.from; + local from = response_data.from or stanza.attr.to; + local reply_stanza = st.stanza(stanza_kind, { + to = to, from = from; + type = response_data.type or (stanza_kind == "message" and "chat") or nil; + }); + if stanza_kind == "message" and response_data.body then + reply_stanza:tag("body"):text(tostring(response_data.body)):up(); + end + module:log("debug", "Sending %s", tostring(reply_stanza)); + return origin.send(reply_stanza); + end + return; + elseif code >= 200 and code <= 299 then + return true; + else + return origin.send(error_reply(stanza, code)); + end + end); + return true; +end + +for stanza_kind in stanza_kinds do + for _, jid_type in ipairs({ "host", "bare", "full" }) do + module:hook(stanza_kind.."/"..jid_type, handle_stanza); + end +end + +-- Simple handler for an always-online JID that allows everyone to subscribe to presence +local function default_presence_handler(event) + local stanza = event.stanza; + module:log("debug", "Handling %s", tostring(stanza)); + if stanza.attr.type == "probe" then + module:send(st.presence({ to = stanza.attr.from, from = stanza.attr.to.."/default" })); + elseif stanza.attr.type == "subscribe" then + module:send(st.presence({ type = "subscribed", to = stanza.attr.from, from = stanza.attr.to.."/default" })); + module:send(st.presence({ to = stanza.attr.from, from = stanza.attr.to.."/default" })); + elseif stanza.attr.type == "unsubscribe" then + module:send(st.presence({ type = "unavailable", to = stanza.attr.from, from = stanza.attr.to.."/default" })); + end + return true; +end + +module:hook("presence/bare", default_presence_handler, -1); diff -r d0ca211e1b0e -r 0f813e22e3fa mod_csi_battery_saver/mod_csi_battery_saver.lua --- a/mod_csi_battery_saver/mod_csi_battery_saver.lua Tue Mar 27 10:48:04 2018 +0200 +++ b/mod_csi_battery_saver/mod_csi_battery_saver.lua Tue Mar 27 10:51:25 2018 +0200 @@ -116,28 +116,28 @@ --session.log("debug", "mod_csi_battery_saver(%s): stanza_direction = %s, carbon = %s, stanza = %s", id, stanza_direction, carbon and "true" or "false", tostring(stanza)); 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 - + -- chat markers (XEP-0333) are important, too, because some clients use them to update their notifications if find(stanza, "{urn:xmpp:chat-markers:0}") then return true; end; - + -- carbon copied outgoing messages are important (some clients update their notifications upon receiving those) --> don't return false here --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" then if stanza:get_child_text("subject") then return true; end diff -r d0ca211e1b0e -r 0f813e22e3fa mod_http_logging/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_http_logging/README.markdown Tue Mar 27 10:51:25 2018 +0200 @@ -0,0 +1,3 @@ +This module produces more detailed HTTP logs for Prosodys built-in HTTP +server. The format is similar to that of Apache and go into Prosodys +normal logs at the `info` level. diff -r d0ca211e1b0e -r 0f813e22e3fa mod_http_upload/README.markdown --- a/mod_http_upload/README.markdown Tue Mar 27 10:48:04 2018 +0200 +++ b/mod_http_upload/README.markdown Tue Mar 27 10:51:25 2018 +0200 @@ -38,6 +38,8 @@ Default is 1MB (1024\*1024). This can not be set over the value of `http_max_content_size` (default 10M). +Consider [mod_http_upload_external] instead of attempting to increase +this limit. ### Max age diff -r d0ca211e1b0e -r 0f813e22e3fa mod_http_upload/mod_http_upload.lua --- a/mod_http_upload/mod_http_upload.lua Tue Mar 27 10:48:04 2018 +0200 +++ b/mod_http_upload/mod_http_upload.lua Tue Mar 27 10:51:25 2018 +0200 @@ -310,7 +310,7 @@ local function serve_hello(event) event.response.headers.content_type = "text/html;charset=utf-8" - return "\n

Hello from mod_"..module.name.."!

\n"; + return "\n

Hello from mod_"..module.name.." on "..module.host.."!

\n"; end module:provides("http", { diff -r d0ca211e1b0e -r 0f813e22e3fa mod_http_upload_external/mod_http_upload_external.lua --- a/mod_http_upload_external/mod_http_upload_external.lua Tue Mar 27 10:48:04 2018 +0200 +++ b/mod_http_upload_external/mod_http_upload_external.lua Tue Mar 27 10:51:25 2018 +0200 @@ -109,7 +109,7 @@ local filename = request.attr.filename; local filesize = tonumber(request.attr.size); local get_url, put_url = handle_request( - origin, stanza, legacy_namespace, filename, filesize); + origin, stanza, namespace, filename, filesize); if not get_url then -- error was already sent diff -r d0ca211e1b0e -r 0f813e22e3fa mod_mam_muc/mod_mam_muc.lua --- a/mod_mam_muc/mod_mam_muc.lua Tue Mar 27 10:48:04 2018 +0200 +++ b/mod_mam_muc/mod_mam_muc.lua Tue Mar 27 10:51:25 2018 +0200 @@ -30,6 +30,10 @@ local new_muc = not rooms; if new_muc then rooms = module:shared"muc/rooms"; +else + -- COMPAT: We don't (currently?) support injecting stanza-id + -- on Prosody 0.10 and prior, which is required by mam:2 + xmlns_mam = "urn:xmpp:mam:1"; end local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or function (jid) diff -r d0ca211e1b0e -r 0f813e22e3fa mod_minimix/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_minimix/README.markdown Tue Mar 27 10:51:25 2018 +0200 @@ -0,0 +1,46 @@ +Account based MUC joining +========================= + +Normally when joining a MUC groupchat, it is each individual client that +joins. This means their presence in the group is tied to the session, +which can be short-lived or unstable, especially in the case of mobile +clients. + +This has a few problems. For one, for every message to the groupchat, a +copy is sent to each joined client. This means that at the account +level, each message would pass by once for each client that is joined, +making it difficult to archive these messages in the users personal +archive. + +A potentially better approach would be that the user account itself is +the entity that joins the groupchat. Since the account is an entity that +lives in the server itself, and the server tends to be online on a good +connection most of the time, this may improve the experience and +simplify some problems. + +This is one of the essential changes in the MIX architecture, which is +being designed to replace MUC. + +`mod_minimix` is an experiment meant to determine if things can be +improved without replacing the entire MUC standard. It works by +pretending to each client that nothing is different and that they are +joining MUCs directly, but behind the scenes, it arranges it such that +only the account itself joins each groupchat. Which sessions have joined +which groups are kept track of. Groupchat messages are then forked to +those sessions, similar to how normal chat messages work. + +Known issues +------------ + +- You can never leave. +- You will never see anyone leave. + +Unknown issues +-------------- + +- Probably many. + +Compatibility +============= + +Briefly tested with Prosody trunk (as of this writing). diff -r d0ca211e1b0e -r 0f813e22e3fa mod_minimix/mod_minimix.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_minimix/mod_minimix.lua Tue Mar 27 10:51:25 2018 +0200 @@ -0,0 +1,116 @@ +-- mod_minimix +-- +-- Rewrite MUC stanzas suich that the account / bare JID joins rooms instead of clients / full JIDs +-- +local jid_split, jid_join, jid_node, jid_bare = import("util.jid", "split", "join", "node", "bare"); +local st = require "util.stanza"; + +local users = prosody.hosts[module.host].sessions; + +local joined_rooms = module:open_store("joined_rooms", "map"); -- TODO cache? +local room_state = module:open_store("joined_rooms_state", "map"); +local all_room_state = module:open_store("joined_rooms_state"); + +-- FIXME You can join but you can never leave. + +module:hook("pre-presence/full", function (event) + local origin, stanza = event.origin, event.stanza; + + local room_node, room_host, nickname = jid_split(stanza.attr.to); + local room_jid = jid_join(room_node, room_host); + local username = origin.username; + + if stanza.attr.type == nil and stanza:get_child("x", "http://jabber.org/protocol/muc") then + module:log("debug", "Joining %s as %s", room_jid, nickname); + + -- TODO Should this be kept track of before the *initial* join has been confirmed or? + if origin.joined_rooms then + origin.joined_rooms[room_jid] = nickname; + else + origin.joined_rooms = { [room_jid] = nickname }; + end + + if joined_rooms:get(username, room_jid) then + module:log("debug", "Already joined to %s as %s", room_jid, nickname); + local state = assert(all_room_state:get(username)); + for jid, stanza in pairs(state) do + if jid ~= room_jid and jid ~= stanza.attr.to then + origin.send(st.clone(st.deserialize(stanza))); + end + end + origin.send(st.deserialize(state[stanza.attr.to])); + origin.send(st.message({type="groupchat",to=origin.full_jid,from=room_jid}):tag("subject"):text(state[room_jid])); + -- Send on-join stanzas from local state, somehow + -- Maybe tell them their nickname was changed if it doesn't match the account one + return true; + end + + joined_rooms:set(username, room_jid, nickname); + + local account_join = st.clone(stanza); + account_join.attr.from = jid_join(origin.username, origin.host); + module:send(account_join); + + return true; + elseif stanza.attr.type == "unavailable" and joined_rooms:get(username, room_jid) then + origin.send(st.reply(stanza)); + return true; + end +end); + +module:hook("pre-message/bare", function (event) + local origin, stanza = event.origin, event.stanza; + local username = origin.username; + local room_jid = jid_bare(stanza.attr.to); + + module:log("info", "%s", stanza) + if joined_rooms:get(username, room_jid) then + local from_account = st.clone(stanza); + from_account.attr.from = jid_join(origin.username, origin.host); + module:log("debug", "Sending:\n%s\nInstead of:\n%s", from_account, stanza); + module:send(from_account, origin); + return true; + end +end); + +local function handle_to_bare_jid(event) + local origin, stanza = event.origin, event.stanza; + local username = jid_node(stanza.attr.to); + local room_jid = jid_bare(stanza.attr.from); + + if joined_rooms:get(username, room_jid) then + module:log("debug", "handle_to_bare_jid %q, %s", room_jid, stanza); + -- Broadcast to clients + + if stanza.name == "message" and stanza.attr.type == "groupchat" + and not stanza:get_child("body") and stanza:get_child("subject") then + room_state:set(username, room_jid, stanza:get_child_text("subject")); + elseif stanza.name == "presence" then + if stanza.attr.type == nil then + room_state:set(username, stanza.attr.from, st.preserialize(stanza)); + elseif stanza.attr.type == "unavailable" then + room_state:set(username, stanza.attr.from, nil); + end + end + + if users[username] then + module:log("debug", "%s has sessions", username); + for _, session in pairs(users[username].sessions) do + module:log("debug", "Session: %s", jid_join(session.username, session.host, session.resource)); + if session.joined_rooms and session.joined_rooms[room_jid] then + module:log("debug", "Is joined"); + local s = st.clone(stanza); + s.attr.to = session.full_jid; + session.send(s); + else + module:log("debug", "session.joined_rooms = %s", session.joined_rooms); + end + end + end + + return true; + end +end + +module:hook("presence/bare", handle_to_bare_jid); +module:hook("message/bare", handle_to_bare_jid); diff -r d0ca211e1b0e -r 0f813e22e3fa mod_muc_gc10/README.markdown --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_muc_gc10/README.markdown Tue Mar 27 10:51:25 2018 +0200 @@ -0,0 +1,18 @@ +# Groupchat 1.0 usage statistics gathering + +Groupchat 1.0 was probably the protocol that predated +[XEP-0045: Multi-User Chat] and there is still some compatibility that +lives on, in the XEP and in implementations. + +This module tries to detect clients still using the GC 1.0 protocol and +what software they run, to determine if support can be removed. + +Since joins in the GC 1.0 protocol are highly ambiguous, some hits +reported will be because of desynchronized MUC clients + +# Compatibility + +Should work with Prosody 0.10.x and earlier. + +It will not work with current trunk, since the MUC code has had major +changes. diff -r d0ca211e1b0e -r 0f813e22e3fa mod_muc_gc10/mod_muc_gc10.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_muc_gc10/mod_muc_gc10.lua Tue Mar 27 10:51:25 2018 +0200 @@ -0,0 +1,58 @@ +local jid_bare = require "util.jid".bare; +local st = require "util.stanza"; + +local rooms = module:depends"muc".rooms; + +module:hook("presence/full", function (event) + local stanza, origin = event.stanza, event.origin; + if stanza.attr.type ~= nil then return end + + local muc_x = stanza:get_child("x", "http://jabber.org/protocol/muc"); + + local room_jid = jid_bare(stanza.attr.to); + local room = rooms[room_jid]; + if not room then + if muc_x then + -- Normal MUC creation + else + module:log("info", "GC 1.0 room creation from %s", stanza.attr.from); + module:send(st.iq({type="get",id=module.name,from=module.host,to=stanza.attr.from}):query("jabber:iq:version")); + end + return; + end + local current_nick = room._jid_nick[stanza.attr.from]; + + if current_nick then + -- present + if muc_x then + module:log("info", "MUC desync with %s", stanza.attr.from); + module:send(st.iq({type="get",id=module.name,from=module.host,to=stanza.attr.from}):query("jabber:iq:version")); + else + -- normal presence update + end + else + -- joining + if muc_x then + -- normal join + else + module:log("info", "GC 1.0 join from %s", stanza.attr.from); + module:send(st.iq({type="get",id=module.name,from=module.host,to=stanza.attr.from}):query("jabber:iq:version")); + end + end +end); + +module:hook("iq-result/host/"..module.name, function (event) + local stanza, origin = event.stanza, event.origin; + local version = stanza:get_child("query", "jabber:iq:version"); + if not version then + module:log("info", "%s replied with an invalid version reply: %s", stanza.attr.from, tostring(stanza)); + return true; + end + module:log("info", "%s is running: %s %s", stanza.attr.from, version:get_child_text("name"), version:get_child_text("version")); +end); + +module:hook("iq-error/host/"..module.name, function (event) + local stanza, origin = event.stanza, event.origin; + module:log("info", "%s replied with an error: %s %s", stanza.attr.from, stanza:get_error()); + return true; +end);