changeset 5128:7cc0f68b8715

mod_unified_push: Experimenal Unified Push provider
author Matthew Wild <mwild1@gmail.com>
date Thu, 05 Jan 2023 17:28:06 +0000
parents be859bfdd44e
children cde38b7de04a
files mod_unified_push/README.md mod_unified_push/mod_unified_push.lua
diffstat 2 files changed, 122 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_unified_push/README.md	Thu Jan 05 17:28:06 2023 +0000
@@ -0,0 +1,33 @@
+---
+labels:
+- Stage-Alpha
+summary: "Unified Push provider"
+---
+
+This module implements a [Unified Push](https://unifiedpush.org/) Provider
+that uses XMPP to talk to a Push Distributor (e.g. [Conversations](http://codeberg.org/iNPUTmice/Conversations)).
+
+For a server-independent external component, or details about the protocol,
+see [the 'up' project](https://codeberg.org/inputmice/up).
+
+This module and the protocol it implements is at an experimental prototype
+stage.
+
+Note that this module is **not related** to XEP-0357 push notifications for
+XMPP. It does not send push notifications to disconnected XMPP clients. For
+that, see [mod_cloud_notify](https://modules.prosody.im/mod_cloud_notify).
+
+## Configuration
+
+| Name                          | Description                                            | Default               |
+|-------------------------------|--------------------------------------------------------|-----------------------|
+| unified_push_secret           | A random secret string (32+ bytes), used for auth      |                       |
+| unified_push_registration_ttl | Maximum lifetime of a push registration (seconds)      | `86400` (1 day)       |
+
+A random push secret can be generated with the command
+`openssl rand -base64 32`. Changing the secret will invalidate all existing
+push registrations.
+
+## Compatibility
+
+Requires Prosody trunk (not compatible with 0.12).
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_unified_push/mod_unified_push.lua	Thu Jan 05 17:28:06 2023 +0000
@@ -0,0 +1,89 @@
+local unified_push_secret = assert(module:get_option_string("unified_push_secret"), "required option: unified_push_secret");
+local push_registration_ttl = module:get_option_number("unified_push_registration_ttl", 86400);
+
+local base64 = require "util.encodings".base64;
+local datetime = require "util.datetime";
+local jwt_sign, jwt_verify = require "util.jwt".init("HS256", unified_push_secret);
+local st = require "util.stanza";
+local urlencode = require "util.http".urlencode;
+
+local xmlns_up = "http://gultsch.de/xmpp/drafts/unified-push";
+
+module:depends("http");
+
+local function check_sha256(s)
+	if not s then return nil, "no value provided"; end
+	local d = base64.decode(s);
+	if not d then return nil, "invalid base64"; end
+	if #d ~= 32 then return nil, "incorrect decoded length, expected 32"; end
+	return s;
+end
+
+-- Handle incoming registration from XMPP client
+function handle_register(event)
+	local origin, stanza = event.origin, event.stanza;
+	local instance, instance_err = check_sha256(stanza.tags[1].attr.instance);
+	if not instance then
+		return st.error_reply(stanza, "modify", "bad-request", "instance: "..instance_err);
+	end
+	local application, application_err = check_sha256(stanza.tags[1].attr.application);
+	if not application then
+		return st.error_reply(stanza, "modify", "bad-request", "application: "..application_err);
+	end
+	local expiry = os.time() + push_registration_ttl;
+	local url = module:http_url().."/"..urlencode(jwt_sign({
+		instance = instance;
+		application = application;
+		sub = stanza.attr.from;
+		exp = expiry;
+	}));
+	module:log("debug", "New push registration successful");
+	return origin.send(st.reply(stanza):tag("registered", {
+		expiration = datetime.datetime(expiry);
+		endpoint = url;
+		xmlns = xmlns_up;
+	}));
+end
+
+module:hook("iq-set/host/"..xmlns_up..":register", handle_register);
+
+-- Handle incoming POST
+function handle_push(event, subpath)
+	local data, err = jwt_verify(subpath);
+	if not data then
+		module:log("debug", "Received push to unacceptable token (%s)", err);
+		return 404;
+	end
+	local payload = event.request.body;
+	if not payload or payload == "" then
+		return 400;
+	elseif #payload > 4096 then
+		return 413;
+	end
+	local push_iq = st.iq({ type = "set", to = data.sub, id = event.request.id })
+		:text_tag("push", base64.encode(payload), { instance = data.instance, application = data.application, xmlns = xmlns_up });
+	return module:send_iq(push_iq):next(function ()
+		return 201;
+	end, function (error_event)
+		local e_type, e_cond, e_text = error_event.stanza:get_error();
+		if e_cond == "item-not-found" or e_cond == "feature-not-implemented" then
+			module:log("debug", "Push rejected");
+			return 404;
+		elseif e_cond == "service-unavailable" or e_cond == "recipient-unavailable" then
+			return 503;
+		end
+		module:log("warn", "Unexpected push error response: %s/%s/%s", e_type, e_cond, e_text);
+		return 500;
+	end);
+end
+
+module:provides("http", {
+	name = "push";
+	route = {
+		["GET /*"] = function (event)
+			event.response.headers.content_type = "application/json";
+			return [[{"unifiedpush":{"version":1}}]];
+		end;
+		["POST /*"] = handle_push;
+	};
+});