Changeset

2957:0f813e22e3fa

Merge commit
author JC Brand <jc@opkode.com>
date Tue, 27 Mar 2018 10:51:25 +0200
parents 2956:d0ca211e1b0e (current diff) 2955:6140f9a03094 (diff)
children 2958:13acce68a89c
files
diffstat 13 files changed, 471 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- 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
 
--- /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
+<?php 
+
+// Receive and decode message JSON
+$post_data = file_get_contents('php://input');
+$received = json_decode($post_data)->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.
--- /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);
--- 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
--- /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.
--- 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
 
--- 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 "<!DOCTYPE html>\n<h1>Hello from mod_"..module.name.."!</h1>\n";
+	return "<!DOCTYPE html>\n<h1>Hello from mod_"..module.name.." on "..module.host.."!</h1>\n";
 end
 
 module:provides("http", {
--- 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
--- 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)
--- /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).
--- /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);
--- /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.
--- /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);