# HG changeset patch # User Matthew Wild # Date 1672939686 0 # Node ID 7cc0f68b87159b96c49c067ad883fc8d42a769aa # Parent be859bfdd44ed011ffcba274b84310752fda38d0 mod_unified_push: Experimenal Unified Push provider diff -r be859bfdd44e -r 7cc0f68b8715 mod_unified_push/README.md --- /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). diff -r be859bfdd44e -r 7cc0f68b8715 mod_unified_push/mod_unified_push.lua --- /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; + }; +});