feat: upgrade prosody 0.11.6 -> 0.12.6 (milestone 1)

Re-port all source patches onto 0.12.6; three are gone entirely:
moduleapi (the two vnc_muc_fcm modules call core.storagemanager
directly now), mod_admin_telnet and portmanager (replaced by
console_interfaces/http_interfaces config). The muc.lib fork keeps its
four functional changes including the externally consumed
muc-config-sub-mitted event; the operator-precedence hunk was fixed
upstream. mod_muc_mam shrinks to keep-archive-on-room-destroy since
muc_log_expires_after="never" disables cleanup upstream in 0.12.

Delete the bundled mod_smacks fork and the no-op mod_smacks_offline;
core 0.12 smacks supersedes them (options audited, dead smacks_max_old
corrected to smacks_max_old_sessions).

Config/startup: drop legacyauth, run_as_root, daemonize; bosh_ports ->
http_ports; cross_domain_* -> http_cors_override; randomize
component_secret at startup (env-overridable); export
log_slow_events_threshold fallback (latent render bug).

Found while smoke-testing: pin --idn-library=idn (0.12's ICU default
segfaults without ICU data in the image); http became a private
service in 0.12 so 5280 needs http_interfaces to stay public; the
telnet console now depends on mod_admin_socket, whose socket moves to
/var/log/prosody.

Verified: prosodyctl check config clean; boots against Postgres with
empty error log and same port bindings as 0.11; healthcheck green;
telnet console and SQL storage round-trip; 0.11->0.12 schema upgrade
rehearsed on a 0.11-created database with data intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
This commit is contained in:
2026-07-15 17:58:02 +02:00
co-authored by Claude Fable 5
parent 2552a58872
commit bdb9aed9e3
22 changed files with 128 additions and 1575 deletions
+3 -4
View File
@@ -10,14 +10,13 @@ There is no application source beyond Lua modules and shell/config templates —
## Repository layout
- `Dockerfile` — multi-stage build. Builder stage compiles Prosody 0.11.6 from source against Alpine packages;
final stage copies the built `/usr/local/` tree, then layers on `patches/`, `vnctalk/`, and `config/`.
- `Dockerfile` — multi-stage build. Builder stage compiles Prosody 0.12.6 from source against Alpine packages
(with `--idn-library=idn`; the 0.12+ ICU default breaks at runtime because the image ships no ICU data);
final stage copies the built `/usr/local/` tree, then layers on `vnctalk/` and `config/`.
- `patches/` — unified diffs against the pristine Prosody source tree, applied in the Dockerfile builder stage
with `patch -p1 --fuzz=0` before `./configure && make install`. Paths inside each patch are relative to the
source root, so adding/removing a `.patch` file needs no Dockerfile change. `patches/README.md` documents each
patch's intent; `upgrade-plan.md` (repo root) tracks their per-version disposition.
- `vnctalk/mod_smacks/mod_smacks.lua.orig` is the pre-patch reference copy, kept for diffing against the
patched version.
- `vnctalk/` — original VNCtalk-authored Prosody modules (own module directory tree), copied wholesale into
`/usr/local/lib/prosody/modules/` in the image. Some are single `.lua` files, others are subdirectories with
their own `README.markdown`/`README.wiki` (following the upstream prosody-modules convention).
+5 -3
View File
@@ -9,13 +9,15 @@ RUN apk update && apk upgrade && \
libc-dev g++ yarn nagios-plugins-tcp patch && \
rm -rf /var/cache/apk/*
RUN wget https://prosody.im/downloads/source/prosody-0.11.6.tar.gz && tar xvfz prosody-0.11.6.tar.gz
RUN wget https://prosody.im/downloads/source/prosody-0.12.6.tar.gz && tar xvfz prosody-0.12.6.tar.gz
ADD patches /vnc/patches
RUN cd prosody-0.11.6/ && \
RUN cd prosody-0.12.6/ && \
for p in /vnc/patches/*.patch; do patch -p1 --fuzz=0 < "$p"; done
RUN cd prosody-0.11.6/ && ./configure --sysconfdir="/etc/prosody" --no-example-certs && \
# --idn-library=idn: 0.12 defaults to ICU, but the runtime image ships no ICU
# data files (U_FILE_ACCESS_ERROR); stick with libidn as 0.11 did
RUN cd prosody-0.12.6/ && ./configure --sysconfdir="/etc/prosody" --no-example-certs --idn-library=idn && \
make && make install
RUN mkdir -p /vnc && mkdir -p /var/run/prosody/ && \
+20 -15
View File
@@ -2,8 +2,6 @@
---- VNCtalk Prosody/XMPP Settings
--------------------------------------------------------------------------------
daemonize = false;
pidfile = "/var/log/prosody/prosody.pid"; -- Required for init scripts and prosodyctl
-- Enable use of libevent for better performance under high load
@@ -16,8 +14,13 @@ allow_registration = false;
network_default_read_size = 8192; -- Increase buffer size to allow jitsi meet and file transfers. Default 4096.
-- for docker only!
run_as_root = true;
-- expose the telnet admin console (5582) beyond loopback; an external
-- service connects to it to run commands
console_interfaces = { "*" }
-- 'http' (5280, BOSH/websocket behind the ingress) is a private service in
-- 0.12+ and would default to loopback only
http_interfaces = { "*", "::" }
admins = {
"admin@${prosodyDomain}"
@@ -38,7 +41,6 @@ modules_enabled = {
"vcard"; -- Allow users to set vCards
"blocklist"; -- Allow users to block communications with other users
"smacks";
"smacks_offline";
"carbons";
"carbons_copies";
"mam";
@@ -66,7 +68,6 @@ modules_enabled = {
"announce"; -- Send announcement to all online users
"watchregistrations"; -- Alert admins of registrations
"motd"; -- Send a message to users when they log in
"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
"webpresence";
"http_rest";
"csi";
@@ -91,6 +92,10 @@ modules_disabled = {
"register"; -- Allow users to register on this server using a client and change passwords
}
-- mod_admin_socket (a dependency of the telnet console since 0.12) defaults its
-- unix socket into the data directory, which is not writable in this container
admin_socket = "/var/log/prosody/prosody.sock"
--------------------------------------------------------------------------------
---- Logging configuration
--------------------------------------------------------------------------------
@@ -114,9 +119,14 @@ component_interface = "0.0.0.0";
---- BOSH Settings
--------------------------------------------------------------------------------
bosh_ports = { 5280 }
http_ports = { 5280 }
bosh_max_inactivity = 60; --Maximum amount of time in seconds a client may remain silent for, with no requests
cross_domain_bosh = true; --Allow access from scripts on any site with no proxy (requires a modern browser)
-- CORS for BOSH/websocket: replaces the pre-0.12 cross_domain_* options;
-- origins default to * (any site), matching the previous behaviour
http_cors_override = {
bosh = { enabled = true };
websocket = { enabled = true };
}
consider_bosh_secure = true; --Use if proxying HTTPS->HTTP on the server side
bosh_default_hold = 1; --Maximum number of requests the server will hold open for a client
bosh_max_requests = 2; --Maximum number of requests a client may make to the server at a time
@@ -127,7 +137,6 @@ trusted_proxies = { "127.0.0.1" } --A list of proxies to trust, used to find a
---- Websocket Settings
--------------------------------------------------------------------------------
cross_domain_websocket = true; --Allow access from scripts on any site with no proxy (requires a modern browser)
consider_websocket_secure = true; --Use if proxying HTTPS->HTTP on the server side
--------------------------------------------------------------------------------
@@ -162,7 +171,6 @@ authentication = "internal_hashed"
---- Storage Settings
--------------------------------------------------------------------------------
default_storage = "sql"
storage = "sql"
sql = {
@@ -186,7 +194,6 @@ pep_max_items = 256
--------------------------------------------------------------------------------
max_archive_query_results = 20;
archive_cleanup_interval = 4*60*60;
archive_expires_after = "never"
default_archive_policy = "roster"
@@ -197,8 +204,7 @@ smacks_hibernation_time = 300;
smacks_enabled_s2s = false;
smacks_max_unacked_stanzas = 5;
smacks_max_ack_delay = 60;
smacks_max_hibernated_sessions = 10;
smacks_max_old = 10;
smacks_max_old_sessions = 10;
--------------------------------------------------------------------------------
log_slow_events_threshold = ${log_slow_events_threshold}
@@ -288,8 +294,7 @@ VirtualHost "anon.${prosodyDomain}"
Component "conference.${prosodyDomain}" "muc"
-- TODO: create random secret in startup
component_secret = "Weew0Ooheivei3aizohmalohLieshohs"
component_secret = "${componentSecret}"
modules_enabled = {
"muc_mam",
"vnc_muc_automember",
+8
View File
@@ -36,12 +36,20 @@ fi
if [ -z "$log_slow_events_threshold" ]; then
log_slow_events_threshold="1.5"
fi
export log_slow_events_threshold
if [ -z "$DEFAULT_JITSI_CONFERENCE" ]; then
DEFAULT_JITSI_CONFERENCE="conference.jitsi.dev.vnc.de"
export DEFAULT_JITSI_CONFERENCE
fi
# secret for external component connections (port 5347); random per container
# start unless supplied via environment
if [ -z "$componentSecret" ]; then
componentSecret=$(openssl rand -hex 24)
fi
export componentSecret
chmod 444 /etc/prosody/certs/prosody-ssl.pem
chmod 444 /etc/prosody/certs/prosody-ssl.key
+13 -6
View File
@@ -1,6 +1,6 @@
# Prosody source patches
Unified diffs against pristine **prosody-0.11.6**, applied in the Dockerfile builder stage with
Unified diffs against pristine **prosody-0.12.6**, applied in the Dockerfile builder stage with
`patch -p1` from the extracted source root, *before* `./configure && make install`. Paths inside
each patch (`a/...` / `b/...`) are relative to the source root, so no separate mapping file is
needed. `patch --fuzz=0` is used so any upstream drift fails the build loudly instead of
@@ -15,14 +15,21 @@ To change a patch: extract the pristine tarball, apply all patches, edit, and re
|-------|--------|--------------------|
| `mod_carbons.patch` | `plugins/mod_carbons.lua` | Hook `vnc-rest-message` (fired by `mod_http_rest`) so REST-injected messages are carbon-copied. |
| `mod_mam.patch` | `plugins/mod_mam/mod_mam.lua` | Archive REST-injected messages (`vnc-rest-message` hook, stored under the *sender*); always store (`shall_store``true`, because users live in the external HTTP auth backend so `user_exists()` can't be trusted); only archive stanzas with a `<body>`. |
| `mod_muc_mam.patch` | `plugins/mod_muc_mam.lua` | Never expire/delete MUC archives: disables `schedule_cleanup()` and archive deletion on room destroy; adds logging. |
| `mod_muc_mam.patch` | `plugins/mod_muc_mam.lua` | Keep MUC archives when a room is destroyed (upstream deletes them). Expiry/cleanup needs no patch since 0.12: `muc_log_expires_after = "never"` (set in the config template) disables the whole cleanup path upstream. |
| `mod_muc.patch` | `plugins/muc/mod_muc.lua` | Route `iq-set/bare/xmpp:vnctalk:unregister:query` to the new `handle_unregister_iq` (see `register.lib.patch`). |
| `register.lib.patch` | `plugins/muc/register.lib.lua` | Add `handle_unregister_iq`: fires `vnc-muc-kick` (consumed by `mod_vnc_track_kicks`) and removes the requester's room affiliation. |
| `muc.lib.patch` | `plugins/muc/muc.lib.lua` | (a) `broadcast()` also routes to offline *remote* affiliated members; (b) suppress the unavailable self-presence in `publicise_occupant_status`; (c) let owner/admin/member post to a room without being a present occupant; (d) fire `muc-config-sub-mitted` on config changes — **consumed by an external service**; (e) operator-precedence fix in the semi-anonymous role-change branch (fixed upstream in 0.12 — drop this hunk at the 0.12 upgrade). |
| `muc.lib.patch` | `plugins/muc/muc.lib.lua` | (a) `broadcast()` also routes to offline *remote* affiliated members; (b) suppress the unavailable self-presence in `publicise_occupant_status`; (c) let owner/admin/member post to a room without being a present occupant (bypasses the `muc-occupant-groupchat` event and its not-in-room rejection); (d) fire `muc-config-sub-mitted` on config changes — **consumed by an external service**. |
| `hidden.lib.patch` | `plugins/muc/hidden.lib.lua` | Hide the "publicly searchable" room config option for everyone when public rooms are restricted (upstream exempts admins). |
| `mod_muc_unique.patch` | `plugins/mod_muc_unique.lua` | Rework muc#unique handler; answer `item-not-found` for bare-JID requests. |
| `moduleapi.patch` | `core/moduleapi.lua` | Add `module:open_host_store(host, name, type)` — used by `mod_vnc_muc_fcm` / `mod_vnc_muc_fcm_hin` to open stores of the VirtualHost from the MUC component. Planned for removal at the 0.12 upgrade (callers can use `core.storagemanager` directly). |
| `mod_admin_telnet.patch` | `plugins/mod_admin_telnet.lua` | Bind the telnet admin console (5582) on `*` instead of loopback — an external service telnets in to run commands. Replaceable by `console_interfaces = { "*" }` config from 0.12 on. |
| `portmanager.patch` | `core/portmanager.lua` | Default private services to `*`/`::` instead of loopback (companion to the telnet exposure). Replaceable by config from 0.12 on. |
## Patches removed at the 0.12.6 upgrade (M1)
- `moduleapi.patch` (`module:open_host_store`) — the two consumers (`mod_vnc_muc_fcm`,
`mod_vnc_muc_fcm_hin`) now call `require "core.storagemanager".open(host, name, type)` directly.
- `mod_admin_telnet.patch` / `portmanager.patch` (bind telnet console on `*`) — replaced by
`console_interfaces = { "*" }` in the config template.
- The operator-precedence hunk formerly in `muc.lib.patch` — fixed upstream in 0.12.
- The `dumpTable`/`table_clone` debug helpers and info-level log spam formerly carried in
several patches — dropped; they had no functional effect.
See `upgrade-plan.md` in the repo root for the per-version disposition of every patch.
-11
View File
@@ -1,11 +0,0 @@
--- a/plugins/mod_admin_telnet.lua
+++ b/plugins/mod_admin_telnet.lua
@@ -20,7 +20,7 @@
local prosody = _G.prosody;
-local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" };
+local console_listener = { default_port = 5582; default_mode = "*a"; interface = "*" };
local iterators = require "util.iterators";
local keys, values = iterators.keys, iterators.values;
+1 -1
View File
@@ -1,6 +1,6 @@
--- a/plugins/mod_carbons.lua
+++ b/plugins/mod_carbons.lua
@@ -108,6 +108,7 @@
@@ -161,6 +161,7 @@
module:hook("pre-message/host", c2s_message_handler, -0.5);
module:hook("pre-message/bare", c2s_message_handler, -0.5);
module:hook("pre-message/full", c2s_message_handler, -0.5);
+14 -14
View File
@@ -1,6 +1,6 @@
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -206,24 +206,7 @@
@@ -275,24 +275,7 @@
end
local function shall_store(user, who)
@@ -11,13 +11,13 @@
- end
- local prefs = get_prefs(user);
- local rule = prefs[who];
- module:log("debug", "%s's rule for %s is %s", user, who, tostring(rule));
- module:log("debug", "%s's rule for %s is %s", user, who, rule);
- if rule ~= nil then
- return rule;
- end
- -- Below could be done by a metatable
- local default = prefs[false];
- module:log("debug", "%s's default rule is %s", user, tostring(default));
- module:log("debug", "%s's default rule is %s", user, default);
- if default == "roster" then
- return has_in_roster(user, who);
- end
@@ -26,18 +26,18 @@
end
local function strip_stanza_id(stanza, user)
@@ -243,7 +226,7 @@
end
@@ -387,7 +370,7 @@
end, -1)
-- Handle messages
-local function message_handler(event, c2s)
+local function message_handler(event, c2s, vnc_rest)
local origin, stanza = event.origin, event.stanza;
local log = c2s and origin.log or module._log;
local orig_type = stanza.attr.type or "normal";
@@ -253,6 +236,9 @@
local orig_from = stanza.attr.from;
@@ -396,6 +379,9 @@
-- Whos storage do we put it in?
-- Whose storage do we put it in?
local store_user = c2s and origin.username or jid_split(orig_to);
+ if vnc_rest then
+ store_user = jid_split(orig_from);
@@ -45,7 +45,7 @@
-- And who are they chatting with?
local with = jid_bare(c2s and orig_to or orig_from);
@@ -292,8 +278,10 @@
@@ -429,8 +415,10 @@
clone_for_storage = stanza;
end
@@ -54,10 +54,10 @@
-- Check with the users preferences
- if shall_store(store_user, with) then
+ if (storebody and shall_store(store_user, with)) then
log("debug", "Archiving stanza: %s", stanza:top_tag());
log("debug", "Archiving stanza: %s (%s)", stanza:top_tag(), why);
-- And stash it
@@ -317,6 +305,10 @@
@@ -475,6 +463,10 @@
return message_handler(event, true);
end
@@ -68,7 +68,7 @@
-- Filter out <stanza-id> before the message leaves the server to prevent privacy leak.
local function strip_stanza_id_after_other_events(event)
event.stanza = strip_stanza_id(event.stanza, event.origin.username);
@@ -413,6 +405,7 @@
@@ -608,6 +600,7 @@
-- Stanzas sent by local clients
module:hook("pre-message/bare", c2s_message_handler, 0);
module:hook("pre-message/full", c2s_message_handler, 0);
@@ -76,8 +76,8 @@
-- Stanzas to local clients
module:hook("message/bare", message_handler, 0);
module:hook("message/full", message_handler, 0);
@@ -421,4 +414,3 @@
(event.reply or event.stanza):tag("feature", {var=xmlns_mam}):up();
@@ -621,4 +614,3 @@
end
(event.reply or event.stanza):tag("feature", {var=xmlns_st_id}):up();
end);
-
+4 -28
View File
@@ -1,38 +1,14 @@
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -18,6 +18,23 @@
-- live_rooms() -> room
-- shutdown_component()
+local function dumpTable(t, depth)
+ local result = "";
+ if (not depth) then depth = 0 end;
+
+ local prefix = string.rep(" ", depth);
+
+ if (type(t) ~= "table") then
+ result = result.." "..tostring(t);
+ else
+ for key,value in pairs(t) do
+ result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
+ end
+ end
+
+ return result;
+end
+
if module:get_host_type() ~= "component" then
error("MUC should be loaded as a component, please see https://prosody.im/doc/components", 0);
end
@@ -85,6 +102,7 @@
@@ -85,6 +85,7 @@
room_mt.get_registered_nick = register.get_registered_nick;
room_mt.get_registered_jid = register.get_registered_jid;
room_mt.handle_register_iq = register.handle_register_iq;
+room_mt.handle_unregister_iq = register.handle_unregister_iq;
local jid_split = require "util.jid".split;
local jid_bare = require "util.jid".bare;
@@ -414,6 +432,7 @@
local presence_broadcast = module:require "muc/presence_broadcast";
room_mt.get_presence_broadcast = presence_broadcast.get;
@@ -444,6 +445,7 @@
["message/bare"] = "handle_message_to_room" ;
["presence/bare"] = "handle_presence_to_room" ;
["iq/bare/jabber:iq:register:query"] = "handle_register_iq";
+1 -24
View File
@@ -1,29 +1,6 @@
--- a/plugins/mod_muc_mam.lua
+++ b/plugins/mod_muc_mam.lua
@@ -326,6 +326,7 @@
-- Handle messages
local function save_to_history(self, stanza)
local room_node, room_host = jid_split(self.jid);
+ module:log("info", "processing stanza for archive: %s @ %s", room_node, room_host);
local stored_stanza = stanza;
@@ -351,11 +352,13 @@
with = with .. "<" .. stanza.attr.type
end
+ module:log("info", "adding stanza to for archive: %s @ %s", room_node, room_host);
-- And stash it
local id, err = archive:append(room_node, nil, stored_stanza, time_now(), with);
if id then
- schedule_cleanup(room_node);
+ module:log("info", "stanza added to for archive: %s @ %s", room_node, room_host);
+ -- schedule_cleanup(room_node);
stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id }));
else
module:log("error", "Could not archive stanza: %s", err);
@@ -381,8 +384,9 @@
@@ -444,8 +444,9 @@
module:log("warn", "Archived message will persist after a room has been destroyed");
else
module:hook("muc-room-destroyed", function(event)
-14
View File
@@ -1,14 +0,0 @@
--- a/core/moduleapi.lua
+++ b/core/moduleapi.lua
@@ -408,6 +408,11 @@
return require"core.storagemanager".open(self.host, name or self.name, store_type);
end
+function api:open_host_store(host, name, store_type)
+ return require"core.storagemanager".open(host, name or self.name, store_type);
+end
+
+
function api:measure(name, stat_type)
local measure = require "core.statsmanager".measure;
return measure(stat_type, "/"..self.host.."/mod_"..self.name.."/"..name);
+24 -62
View File
@@ -1,34 +1,6 @@
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -31,6 +31,27 @@
local is_kickable_error = muc_util.is_kickable_error;
local valid_roles, valid_affiliations = muc_util.valid_roles, muc_util.valid_affiliations;
+local function dumpTable(t, depth)
+ local result = "";
+ if (not depth) then depth = 0 end;
+
+ local prefix = string.rep(" ", depth);
+
+ if (type(t) ~= "table") then
+ result = result.." "..tostring(t);
+ else
+ for key,value in pairs(t) do
+ result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
+ end
+ end
+
+ return result;
+end
+
+local function table_clone(org)
+ return {table.unpack(org)}
+end
+
local room_mt = {};
room_mt.__index = room_mt;
@@ -200,11 +221,32 @@
@@ -201,11 +201,30 @@
-- Broadcast a stanza to all occupants in the room.
-- optionally checks conditional called with (nick, occupant)
function room_mt:broadcast(stanza, cond_func)
@@ -39,7 +11,6 @@
+ end
+ end
+
+ -- log("info", "cloned xaffs is %s", dumpTable(xaffs));
for nick, occupant in self:each_occupant() do
+ xaffs[occupant.bare_jid] = nil;
if cond_func == nil or cond_func(nick, occupant) then
@@ -47,7 +18,6 @@
end
end
+
+ -- log("info", "remaining xaffs is %s", dumpTable(xaffs));
+ for offlineaff, orol in pairs(xaffs) do
+ local ouser, ohost = jid_split(offlineaff);
+ if not (prosody.hosts[ohost]) then
@@ -61,7 +31,7 @@
end
local function can_see_real_jids(whois, occupant)
@@ -298,7 +340,8 @@
@@ -319,7 +338,8 @@
self_x:tag("status", {code = "110";}):up();
if occupant.role == nil then
-- They get an unavailable
@@ -71,7 +41,7 @@
else
-- use their own presences as templates
for full_jid, pr in occupant:each_session() do
@@ -844,6 +887,7 @@
@@ -971,6 +991,7 @@
return true;
end
module:fire_event("muc-config-submitted", event);
@@ -79,7 +49,7 @@
for submitted_field in pairs(present) do
event.field, event.value = submitted_field, fields[submitted_field];
module:fire_event("muc-config-submitted/"..submitted_field, event);
@@ -859,6 +903,8 @@
@@ -986,6 +1007,8 @@
for code in pairs(event.status_codes) do
msg:tag("status", {code = code;}):up();
end
@@ -88,7 +58,7 @@
msg:up();
self:broadcast_message(msg);
end
@@ -1022,6 +1068,8 @@
@@ -1161,6 +1184,8 @@
return true;
end
function room_mt:handle_owner_query_set_to_room(origin, stanza)
@@ -97,39 +67,31 @@
if self:get_affiliation(stanza.attr.from) ~= "owner" then
origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
return true;
@@ -1049,10 +1097,20 @@
function room_mt:handle_groupchat_to_room(origin, stanza)
local from = stanza.attr.from;
local occupant = self:get_occupant_by_real_jid(from);
- if module:fire_event("muc-occupant-groupchat", {
- room = self; origin = origin; stanza = stanza; from = from; occupant = occupant;
- }) then return true; end
- stanza.attr.from = occupant.nick;
@@ -1195,14 +1220,20 @@
if not stanza.attr.id then
stanza.attr.id = new_id()
end
- local event_data = {room = self; origin = origin; stanza = stanza; from = from; occupant = occupant};
- if module:fire_event("muc-occupant-groupchat", event_data) then
- return true;
- end
- if event_data.occupant then
- stanza.attr.from = event_data.occupant.nick;
+ local from_res = jid_resource(stanza.attr.from);
+ local affiliate = self:get_affiliation(from_res) or "none";
+ -- module:log("debug", "affiliation: %s", affiliate);
+ local aff_allowed = false;
+ if (affiliate == "owner" or affiliate == "member" or affiliate == "admin") then
+ aff_allowed = true;
+ else
+ if module:fire_event("muc-occupant-groupchat", {
+ room = self; origin = origin; stanza = stanza; from = from; occupant = occupant;
+ }) then return true; end
else
- stanza.attr.from = self.jid;
+ local event_data = {room = self; origin = origin; stanza = stanza; from = from; occupant = occupant};
+ if module:fire_event("muc-occupant-groupchat", event_data) then
+ return true;
+ end
+ occupant = event_data.occupant;
+ end
+ if occupant then
+ stanza.attr.from = occupant.nick;
+ end
+ stanza.attr.from = occupant.nick;
end
self:broadcast_message(stanza);
stanza.attr.from = from;
return true;
@@ -1326,8 +1384,8 @@
if occupant.role == nil then
module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
elseif is_semi_anonymous and
- (old_role == "moderator" and occupant.role ~= "moderator") or
- (old_role ~= "moderator" and occupant.role == "moderator") then -- Has gained or lost moderator status
+ ((old_role == "moderator" and occupant.role ~= "moderator") or
+ (old_role ~= "moderator" and occupant.role == "moderator")) then -- Has gained or lost moderator status
-- Send everyone else's presences (as jid visibility has changed)
for real_jid in occupant:each_session() do
self:send_occupant_list(real_jid, function(occupant_jid, occupant) --luacheck: ignore 212 433
-16
View File
@@ -1,16 +0,0 @@
--- a/core/portmanager.lua
+++ b/core/portmanager.lua
@@ -23,11 +23,11 @@
local default_local_interfaces = { };
if config.get("*", "use_ipv4") ~= false then
table.insert(default_interfaces, "*");
- table.insert(default_local_interfaces, "127.0.0.1");
+ table.insert(default_local_interfaces, "*");
end
if socket.tcp6 and config.get("*", "use_ipv6") ~= false then
table.insert(default_interfaces, "::");
- table.insert(default_local_interfaces, "::1");
+ table.insert(default_local_interfaces, "::");
end
local default_mode = config.get("*", "network_default_read_size") or 4096;
+1 -25
View File
@@ -1,30 +1,6 @@
--- a/plugins/muc/register.lib.lua
+++ b/plugins/muc/register.lib.lua
@@ -8,6 +8,23 @@
local enforce_nick = module:get_option_boolean("enforce_registered_nickname", false);
+local function dumpTable(t, depth)
+ local result = "";
+ if (not depth) then depth = 0 end;
+
+ local prefix = string.rep(" ", depth);
+
+ if (type(t) ~= "table") then
+ result = result.." "..tostring(t);
+ else
+ for key,value in pairs(t) do
+ result = result..prefix..tostring(key).." => "..dumpTable(value, depth+1).."\n"
+ end
+ end
+
+ return result;
+end
+
-- reserved_nicks[nick] = jid
local function get_reserved_nicks(room)
if room._reserved_nicks then
@@ -186,8 +203,27 @@
@@ -228,8 +228,27 @@
return true;
end
+24 -1
View File
@@ -117,7 +117,30 @@ Version-sensitive API usage found by scan (everything else in these modules uses
| All MUC-related `mod_vnc_*` | Hook `muc-broadcast-message`, `muc-config-submitted`, `muc-disco#info`, `muc-invite`, `muc-occupant-session-new` | All verified still fired in 0.12.5 and 13.0.6 — re-test, no rewrite expected |
| Modules doing outbound HTTP (fcm, delfile, vcard_avatar, auth) | `net.http.request` old signature | Unchanged in 13.0.6; no action, just testsuite coverage |
## Milestone 1: 0.11.6 → 0.12.5, Lua 5.2
## Milestone 1: 0.11.6 → 0.12.6, Lua 5.2 — ✅ IMPLEMENTED
Status: all repo changes done and smoke-tested (0.12.6 was latest 0.12.x at execution time).
Verified locally: image builds with all 8 remaining patches applying at `--fuzz=0`;
`prosodyctl check config` passes clean; boots against Postgres with an empty error log; c2s/s2s/
http/https/console services bind the same interfaces as 0.11 did; healthcheck passes; telnet
console answers commands on 5582; SQL storage round-trips (tables auto-created); and the
0.11→0.12 `prosodyctl mod_storage_sql upgrade` was rehearsed against a real 0.11-created
database (non-interactive runs need `echo y |` or a TTY; archive data intact afterwards).
Discovered during execution (now part of the changes):
- 0.12 switched the default IDN library to ICU, which broke at runtime (`U_FILE_ACCESS_ERROR`
plus a segfault) because the image ships no ICU data — pinned `--idn-library=idn`.
- `http` (5280) became a *private* service in 0.12 and bound loopback-only — this, not just the
telnet console, is what `portmanager.patch` had been widening. Fixed via
`http_interfaces = { "*", "::" }`.
- `mod_admin_socket` is a hard dependency of the telnet console in 0.12 and its unix socket
defaults into the unwritable data dir — pointed `admin_socket` at `/var/log/prosody/`.
- Latent `startup.sh` bug: the `log_slow_events_threshold` fallback wasn't exported, so the
rendered config was syntactically invalid whenever the env var was absent — fixed.
Remaining (external, before rollout): run the external testsuite against the M1 image; exercise
the telnet service's exact command set (console was reimplemented on mod_admin_shell in 0.12 —
syntax/output changed); dump the production DB and run the schema upgrade per step 5 below.
1. **Dockerfile**: bump source URL/version; `./configure` flags unchanged (`--sysconfdir`,
`--no-example-certs` still valid). Keep all lua5.2 Alpine packages as-is.
-63
View File
@@ -1,63 +0,0 @@
---
labels:
- 'Stage-Alpha'
summary: 'XEP-0198: Reliability and fast reconnects for XMPP'
...
Introduction
============
By default XMPP is as reliable as your network is. Unfortunately in some
cases that is not very reliable - in some network conditions disconnects
can be frequent and message loss can occur.
To overcome this, XMPP has an optional extension (XEP-0198: Stream
Management) which, when supported by both the client and server, can
allow a client to resume a disconnected session, and prevent message
loss.
Details
=======
When using XEP-0198 both the client and the server keep a queue of the
most recently sent stanzas - this is cleared when the other end
acknowledges they have received the stanzas. If the client disconnects,
instead of marking the user offline the server pretends the client is
still online for a short (configurable) period of time. If the client
reconnects within this period, any stanzas in the queue that the client
did not receive are re-sent.
If the client fails to reconnect before the timeout then it is marked
offline as normal, and any stanzas in the queue are returned to the
sender as a "recipient-unavailable" error.
Configuration
=============
Option Default Description
------------------------------ ----------------- -----------------------------------------------------------------------------------------
`smacks_hibernation_time` 300 (5 minutes) The number of seconds a disconnected session should stay alive for (to allow reconnect)
`smacks_enabled_s2s` false Enable Stream Management on server connections? *Experimental*
`smacks_max_unacked_stanzas` 0 How many stanzas to send before requesting acknowledgement
Compatibility
=============
----- -----------------------------------
0.9 Works
0.8 Works, use version [7693724881b3]
----- -----------------------------------
Clients
=======
Clients that support XEP-0198:
- Gajim
- Swift (but not resumption, as of version 2.0 and alphas of 3.0)
- Psi (in an unreleased branch)
- Conversations
- Yaxim
[7693724881b3]: //hg.prosody.im/prosody-modules/raw-file/7693724881b3/mod_smacks/mod_smacks.lua
-31
View File
@@ -1,31 +0,0 @@
#summary XEP-0198: Reliability and fast reconnects for XMPP
#labels Stage-Beta
= Introduction =
By default XMPP is as reliable as your network is. Unfortunately in some cases that is not very reliable - in some network conditions disconnects can be frequent and message loss can occur.
To overcome this, XMPP has an optional extension (XEP-0198: Stream Management) which, when supported by both the client and server, can allow a client to resume a disconnected session, and prevent message loss.
= Details =
When using XEP-0198 both the client and the server keep a queue of the most recently sent stanzas - this is cleared when the other end acknowledges they have received the stanzas. If the client disconnects, instead of marking the user offline the server pretends the client is still online for a short (configurable) period of time. If the client reconnects within this period, any stanzas in the queue that the client did not receive are re-sent.
If the client fails to reconnect before the timeout then it is marked offline as normal, and any stanzas in the queue are returned to the sender as a "recipient-unavailable" error.
= Configuration =
|| *Option* || *Default* || *Description* ||
|| smacks_hibernation_time || 300 (5 minutes) || The number of seconds a disconnected session should stay alive for (to allow reconnect) ||
= Compatibility =
||0.9||Works||
||0.8||Works, use version [http://prosody-modules.googlecode.com/hg-history/7693724881b3f3cdafa35763f00dd040d02313bf/mod_smacks/mod_smacks.lua 7693724881b3]||
= Clients =
Clients that support XEP-0198:
* Gajim
* Swift (but not resumption, as of version 2.0 and alphas of 3.0)
* Psi (in an unreleased branch)
* Yaxim
-659
View File
@@ -1,659 +0,0 @@
-- XEP-0198: Stream Management for Prosody IM
--
-- Copyright (C) 2010-2015 Matthew Wild
-- Copyright (C) 2010 Waqas Hussain
-- Copyright (C) 2012-2015 Kim Alvefur
-- Copyright (C) 2012 Thijs Alkemade
-- Copyright (C) 2014 Florian Zeitz
-- Copyright (C) 2016-2020 Thilo Molitor
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local st = require "util.stanza";
local dep = require "util.dependencies";
local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
local uuid_generate = require "util.uuid".generate;
local jid = require "util.jid";
local t_remove = table.remove;
local math_min = math.min;
local math_max = math.max;
local os_time = os.time;
local tonumber, tostring = tonumber, tostring;
local add_filter = require "util.filters".add_filter;
local timer = require "util.timer";
local datetime = require "util.datetime";
local xmlns_sm2 = "urn:xmpp:sm:2";
local xmlns_sm3 = "urn:xmpp:sm:3";
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
local xmlns_delay = "urn:xmpp:delay";
local sm2_attr = { xmlns = xmlns_sm2 };
local sm3_attr = { xmlns = xmlns_sm3 };
local resume_timeout = module:get_option_number("smacks_hibernation_time", 600);
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30);
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
local core_process_stanza = prosody.core_process_stanza;
local sessionmanager = require"core.sessionmanager";
assert(max_hibernated_sessions > 0, "smacks_max_hibernated_sessions must be greater than 0");
assert(max_old_sessions > 0, "smacks_max_old_sessions must be greater than 0");
local c2s_sessions = module:shared("/*/c2s/sessions");
local function init_session_cache(max_entries, evict_callback)
-- old prosody version < 0.10 (no limiting at all!)
if not cache then
local store = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
return store[key];
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
store[key] = value;
end;
};
end
-- use per user limited cache for prosody >= 0.10
local stores = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then
stores[user] = cache.new(max_entries, evict_callback);
end
return stores[user]:get(key);
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then stores[user] = cache.new(max_entries, evict_callback); end
stores[user]:set(key, value);
-- remove empty caches completely
if not stores[user]:count() then stores[user] = nil; end
end;
};
end
local old_session_registry = init_session_cache(max_old_sessions, nil);
local session_registry = init_session_cache(max_hibernated_sessions, function(resumption_token, session)
if session.destroyed then return true; end -- destroyed session can always be removed from cache
session.log("warn", "User has too much hibernated sessions, removing oldest session (token: %s)", resumption_token);
-- store old session's h values on force delete
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
return true; -- allow session to be removed from full cache to make room for new one
end);
local function stoppable_timer(delay, callback)
local stopped = false;
local timer = module:add_timer(delay, function (t)
if stopped then return; end
return callback(t);
end);
if timer and timer.stop then return timer; end -- new prosody api includes stop() function
return {
stop = function(self) stopped = true end;
timer;
};
end
local function delayed_ack_function(session)
-- fire event only if configured to do so and our session is not already hibernated or destroyed
if delayed_ack_timeout > 0 and session.awaiting_ack
and not session.hibernating and not session.destroyed then
session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d",
session.outgoing_stanza_queue and #session.outgoing_stanza_queue or 0);
module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue});
end
session.delayed_ack_timer = nil;
end
local function can_do_smacks(session, advertise_only)
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
local session_type = session.type;
if session.username then
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
end
return true;
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
return true;
end
return false, "service-unavailable", "Stream management is not available for this stream";
end
module:hook("stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
module:hook("s2s-stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
local function request_ack_if_needed(session, force, reason)
local queue = session.outgoing_stanza_queue;
local expected_h = session.last_acknowledged_stanza + #queue;
-- session.log("debug", "*** SMACKS(1) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
if session.awaiting_ack == nil and not session.hibernating then
-- this check of last_requested_h prevents ack-loops if missbehaving clients report wrong
-- stanza counts. it is set when an <r> is really sent (e.g. inside timer), preventing any
-- further requests until a higher h-value would be expected.
-- session.log("debug", "*** SMACKS(2) ***: #queue=%s, max_unacked_stanzas=%s, expected_h=%s, last_requested_h=%s", tostring(#queue), tostring(max_unacked_stanzas), tostring(expected_h), tostring(session.last_requested_h));
if (#queue > max_unacked_stanzas and expected_h ~= session.last_requested_h) or force then
session.log("debug", "Queuing <r> (in a moment) from %s - #queue=%d", reason, #queue);
session.awaiting_ack = false;
session.awaiting_ack_timer = stoppable_timer(1e-06, function ()
-- session.log("debug", "*** SMACKS(3) ***: awaiting_ack=%s, hibernating=%s", tostring(session.awaiting_ack), tostring(session.hibernating));
-- only request ack if needed and our session is not already hibernated or destroyed
if not session.awaiting_ack and not session.hibernating and not session.destroyed then
session.log("debug", "Sending <r> (inside timer, before send) from %s - #queue=%d", reason, #queue);
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
session.awaiting_ack = true;
-- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile)
session.last_requested_h = session.last_acknowledged_stanza + #queue;
session.log("debug", "Sending <r> (inside timer, after send) from %s - #queue=%d", reason, #queue);
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
end
end
end);
end
end
-- Trigger "smacks-ack-delayed"-event if we added new (ackable) stanzas to the outgoing queue
-- and there isn't already a timer for this event running.
-- If we wouldn't do this, stanzas added to the queue after the first "smacks-ack-delayed"-event
-- would not trigger this event (again).
if #queue > max_unacked_stanzas and session.awaiting_ack and session.delayed_ack_timer == nil then
session.log("debug", "Calling delayed_ack_function directly (still waiting for ack)");
delayed_ack_function(session);
end
end
local function outgoing_stanza_filter(stanza, session)
-- XXX: Normally you wouldn't have to check the xmlns for a stanza as it's
-- supposed to be nil.
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
-- stanzas/out filter can get called before this one and adds the xmlns.
local is_stanza = stanza.attr and
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
and not stanza.name:find":";
if is_stanza and not stanza._cached then
local queue = session.outgoing_stanza_queue;
local cached_stanza = st.clone(stanza);
cached_stanza._cached = true;
if cached_stanza and cached_stanza.name ~= "iq" and cached_stanza:get_child("delay", xmlns_delay) == nil then
cached_stanza = cached_stanza:tag("delay", {
xmlns = xmlns_delay,
from = jid.bare(session.full_jid or session.host),
stamp = datetime.datetime()
});
end
queue[#queue+1] = cached_stanza;
if session.hibernating then
session.log("debug", "hibernating, stanza queued");
module:fire_event("smacks-hibernation-stanza-queued", {origin = session, queue = queue, stanza = cached_stanza});
return nil;
end
request_ack_if_needed(session, false, "outgoing_stanza_filter");
end
return stanza;
end
local function count_incoming_stanzas(stanza, session)
if not stanza.attr.xmlns then
session.handled_stanza_count = session.handled_stanza_count + 1;
session.log("debug", "Handled %d incoming stanzas", session.handled_stanza_count);
end
return stanza;
end
local function wrap_session_out(session, resume)
if not resume then
session.outgoing_stanza_queue = {};
session.last_acknowledged_stanza = 0;
end
add_filter(session, "stanzas/out", outgoing_stanza_filter, -999);
local session_close = session.close;
function session.close(...)
if session.resumption_token then
session_registry.set(session.username, session.resumption_token, nil);
old_session_registry.set(session.username, session.resumption_token, nil);
session.resumption_token = nil;
end
-- send out last ack as per revision 1.5.2 of XEP-0198
if session.smacks and session.conn then
(session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks, h = string.format("%d", session.handled_stanza_count) }));
end
return session_close(...);
end
return session;
end
local function wrap_session_in(session, resume)
if not resume then
session.handled_stanza_count = 0;
end
add_filter(session, "stanzas/in", count_incoming_stanzas, 999);
return session;
end
local function wrap_session(session, resume)
wrap_session_out(session, resume);
wrap_session_in(session, resume);
return session;
end
function handle_enable(session, stanza, xmlns_sm)
local ok, err, err_text = can_do_smacks(session);
if not ok then
session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
return true;
end
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session(session, false);
local resume_token;
local resume = stanza.attr.resume;
if resume == "true" or resume == "1" then
resume_token = uuid_generate();
session_registry.set(session.username, resume_token, session);
session.resumption_token = resume_token;
end
(session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = tostring(resume_timeout) }));
return true;
end
module:hook_stanza(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
module:hook_stanza("http://etherx.jabber.org/streams", "features",
function (session, stanza)
stoppable_timer(1e-6, function ()
if can_do_smacks(session) then
if stanza:get_child("sm", xmlns_sm3) then
session.sends2s(st.stanza("enable", sm3_attr));
session.smacks = xmlns_sm3;
elseif stanza:get_child("sm", xmlns_sm2) then
session.sends2s(st.stanza("enable", sm2_attr));
session.smacks = xmlns_sm2;
else
return;
end
wrap_session_out(session, false);
end
end);
end);
function handle_enabled(session, stanza, xmlns_sm)
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session_in(session, false);
-- FIXME Resume?
return true;
end
module:hook_stanza(xmlns_sm2, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm3); end, 100);
function handle_r(origin, stanza, xmlns_sm)
if not origin.smacks then
module:log("debug", "Received ack request from non-smack-enabled session");
return;
end
module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
-- Reply with <a>
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = string.format("%d", origin.handled_stanza_count) }));
-- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h)
local expected_h = origin.last_acknowledged_stanza + #origin.outgoing_stanza_queue;
if #origin.outgoing_stanza_queue > 0 and expected_h ~= origin.last_requested_h then
request_ack_if_needed(origin, true, "piggybacked by handle_r");
end
return true;
end
module:hook_stanza(xmlns_sm2, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm3); end);
function handle_a(origin, stanza)
if not origin.smacks then return; end
origin.awaiting_ack = nil;
if origin.awaiting_ack_timer then
origin.awaiting_ack_timer:stop();
end
if origin.delayed_ack_timer then
origin.delayed_ack_timer:stop();
origin.delayed_ack_timer = nil;
end
-- Remove handled stanzas from outgoing_stanza_queue
-- origin.log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or "");
local h = tonumber(stanza.attr.h);
if not h then
origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; };
return;
end
local handled_stanza_count = h-origin.last_acknowledged_stanza;
local queue = origin.outgoing_stanza_queue;
if handled_stanza_count > #queue then
origin.log("warn", "The client says it handled %d new stanzas, but we only sent %d :)",
handled_stanza_count, #queue);
origin.log("debug", "Client h: %d, our h: %d", tonumber(stanza.attr.h), origin.last_acknowledged_stanza);
for i=1,#queue do
origin.log("debug", "Q item %d: %s", i, tostring(queue[i]));
end
end
for i=1,math_min(handled_stanza_count,#queue) do
local handled_stanza = t_remove(origin.outgoing_stanza_queue, 1);
module:fire_event("delivery/success", { session = origin, stanza = handled_stanza });
end
origin.log("debug", "#queue = %d", #queue);
origin.last_acknowledged_stanza = origin.last_acknowledged_stanza + handled_stanza_count;
request_ack_if_needed(origin, false, "handle_a")
return true;
end
module:hook_stanza(xmlns_sm2, "a", handle_a);
module:hook_stanza(xmlns_sm3, "a", handle_a);
--TODO: Optimise... incoming stanzas should be handled by a per-session
-- function that has a counter as an upvalue (no table indexing for increments,
-- and won't slow non-198 sessions). We can also then remove the .handled flag
-- on stanzas
local function handle_unacked_stanzas(session)
local queue = session.outgoing_stanza_queue;
local error_attr = { type = "cancel" };
if #queue > 0 then
session.outgoing_stanza_queue = {};
for i=1,#queue do
if not module:fire_event("delivery/failure", { session = session, stanza = queue[i] }) then
if queue[i].attr.type ~= "error" then
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
end
end
end
end
end
end
-- don't send delivery errors for messages which will be delivered by mam later on
-- check if stanza was archived --> this will allow us to send back errors for stanzas not archived
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
local function get_stanza_id(stanza, by_jid)
for tag in stanza:childtags("stanza-id", "urn:xmpp:sid:0") do
if tag.attr.by == by_jid then
return tag.attr.id;
end
end
return nil;
end
module:hook("delivery/failure", function(event)
local session, stanza = event.session, event.stanza;
-- Only deal with authenticated (c2s) sessions
if session.username then
if stanza.name == "message" and stanza.attr.xmlns == nil and
( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then
-- do nothing here for normal messages and don't send out "message delivery errors",
-- because messages are already in MAM at this point (no need to frighten users)
local stanza_id = get_stanza_id(stanza, jid.bare(session.full_jid));
if session.mam_requested and stanza_id ~= nil then
session.log("debug", "mod_smacks delivery/failuere returning true for mam-handled stanza: mam-archive-id=%s", tostring(stanza_id));
return true; -- stanza handled, don't send an error
end
-- store message in offline store, if this client does not use mam *and* was the last client online
local sessions = prosody.hosts[module.host].sessions[session.username] and
prosody.hosts[module.host].sessions[session.username].sessions or nil;
if sessions and next(sessions) == session.resource and next(sessions, session.resource) == nil then
local ok = module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
session.log("debug", "mod_smacks delivery/failuere returning %s for offline-handled stanza", tostring(ok));
return ok; -- if stanza was handled, don't send an error
end
end
end
end);
module:hook("pre-resource-unbind", function (event)
local session, err = event.session, event.error;
if session.smacks then
if not session.resumption_token then
local queue = session.outgoing_stanza_queue;
if #queue > 0 then
session.log("debug", "Destroying session with %d unacked stanzas", #queue);
handle_unacked_stanzas(session);
end
else
session.log("debug", "mod_smacks hibernating session for up to %d seconds", resume_timeout);
local hibernate_time = os_time(); -- Track the time we went into hibernation
session.hibernating = hibernate_time;
local resumption_token = session.resumption_token;
module:fire_event("smacks-hibernation-start", {origin = session, queue = session.outgoing_stanza_queue});
timer.add_task(resume_timeout, function ()
session.log("debug", "mod_smacks hibernation timeout reached...");
-- We need to check the current resumption token for this resource
-- matches the smacks session this timer is for in case it changed
-- (for example, the client may have bound a new resource and
-- started a new smacks session, or not be using smacks)
local curr_session = full_sessions[session.full_jid];
if session.destroyed then
session.log("debug", "The session has already been destroyed");
elseif curr_session and curr_session.resumption_token == resumption_token
-- Check the hibernate time still matches what we think it is,
-- otherwise the session resumed and re-hibernated.
and session.hibernating == hibernate_time then
-- wait longer if the timeout isn't reached because push was enabled for this session
-- session.first_hibernated_push is the starting point for hibernation timeouts of those push enabled clients
-- wait for an additional resume_timeout seconds if no push occured since hibernation at all
local current_time = os_time();
local timeout_start = math_max(session.hibernating, session.first_hibernated_push or session.hibernating);
if session.push_identifier ~= nil and not session.first_hibernated_push then
session.log("debug", "No push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout);
return resume_timeout;
end
if current_time-timeout_start < resume_timeout and session.push_identifier ~= nil then
session.log("debug", "A push happened since hibernation started, hibernating session for up to %d extra seconds", current_time-timeout_start);
return current_time-timeout_start; -- time left to wait
end
session.log("debug", "Destroying session for hibernating too long");
session_registry.set(session.username, session.resumption_token, nil);
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, session.resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
session.resumption_token = nil;
sessionmanager.destroy_session(session);
else
session.log("debug", "Session resumed before hibernation timeout, all is well")
end
end);
return true; -- Postpone destruction for now
end
end
end);
local function handle_s2s_destroyed(event)
local session = event.session;
local queue = session.outgoing_stanza_queue;
if queue and #queue > 0 then
session.log("warn", "Destroying session with %d unacked stanzas", #queue);
if s2s_resend then
for i = 1, #queue do
module:send(queue[i]);
end
session.outgoing_stanza_queue = nil;
else
handle_unacked_stanzas(session);
end
end
end
module:hook("s2sout-destroyed", handle_s2s_destroyed);
module:hook("s2sin-destroyed", handle_s2s_destroyed);
local function get_session_id(session)
return session.id or (tostring(session):match("[a-f0-9]+$"));
end
function handle_resume(session, stanza, xmlns_sm)
if session.full_jid then
session.log("warn", "Tried to resume after resource binding");
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("unexpected-request", { xmlns = xmlns_errors })
);
return true;
end
local id = stanza.attr.previd;
local original_session = session_registry.get(session.username, id);
if not original_session then
session.log("debug", "Tried to resume non-existent session with id %s", id);
local old_session = old_session_registry.get(session.username, id);
if old_session and session.username == old_session.username
and session.host == old_session.host
and old_session.h then
session.send(st.stanza("failed", { xmlns = xmlns_sm, h = string.format("%d", old_session.h) })
:tag("item-not-found", { xmlns = xmlns_errors })
);
else
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("item-not-found", { xmlns = xmlns_errors })
);
end;
elseif session.username == original_session.username
and session.host == original_session.host then
session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session));
-- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
if original_session.conn then
original_session.log("debug", "mod_smacks closing an old connection for this session");
local conn = original_session.conn;
c2s_sessions[conn] = nil;
conn:close();
end
local migrated_session_log = session.log;
original_session.ip = session.ip;
original_session.conn = session.conn;
original_session.send = session.send;
original_session.close = session.close;
original_session.filter = session.filter;
original_session.filter.session = original_session;
original_session.filters = session.filters;
original_session.stream = session.stream;
original_session.secure = session.secure;
original_session.hibernating = nil;
session.log = original_session.log;
session.type = original_session.type;
wrap_session(original_session, true);
-- Inform xmppstream of the new session (passed to its callbacks)
original_session.stream:set_session(original_session);
-- Similar for connlisteners
c2s_sessions[session.conn] = original_session;
original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
h = string.format("%d", original_session.handled_stanza_count), previd = id }));
-- Fake an <a> with the h of the <resume/> from the client
original_session:dispatch_stanza(st.stanza("a", { xmlns = xmlns_sm,
h = stanza.attr.h }));
-- 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;
session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", #queue);
for i=1,#queue do
session.send(queue[i]);
end
session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", #queue);
function session.send(stanza)
migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza));
return false;
end
module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue});
request_ack_if_needed(original_session, true, "handle_resume");
else
module:log("warn", "Client %s@%s[%s] tried to resume stream for %s@%s[%s]",
session.username or "?", session.host or "?", session.type,
original_session.username or "?", original_session.host or "?", original_session.type);
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("not-authorized", { xmlns = xmlns_errors }));
end
return true;
end
module:hook_stanza(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
local function handle_read_timeout(event)
local session = event.session;
if session.smacks then
if session.awaiting_ack then
if session.awaiting_ack_timer then
session.awaiting_ack_timer:stop();
end
if session.delayed_ack_timer then
session.delayed_ack_timer:stop();
session.delayed_ack_timer = nil;
end
return false; -- Kick the session
end
session.log("debug", "Sending <r> (read timeout)");
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
session.awaiting_ack = true;
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
end
return true;
end
end
module:hook("s2s-read-timeout", handle_read_timeout);
module:hook("c2s-read-timeout", handle_read_timeout);
-586
View File
@@ -1,586 +0,0 @@
-- XEP-0198: Stream Management for Prosody IM
--
-- Copyright (C) 2010-2015 Matthew Wild
-- Copyright (C) 2010 Waqas Hussain
-- Copyright (C) 2012-2015 Kim Alvefur
-- Copyright (C) 2012 Thijs Alkemade
-- Copyright (C) 2014 Florian Zeitz
-- Copyright (C) 2016-2017 Thilo Molitor
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local st = require "util.stanza";
local dep = require "util.dependencies";
local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
local uuid_generate = require "util.uuid".generate;
local jid = require "util.jid";
local t_insert, t_remove = table.insert, table.remove;
local math_min = math.min;
local math_max = math.max;
local os_time = os.time;
local tonumber, tostring = tonumber, tostring;
local add_filter = require "util.filters".add_filter;
local timer = require "util.timer";
local datetime = require "util.datetime";
local xmlns_sm2 = "urn:xmpp:sm:2";
local xmlns_sm3 = "urn:xmpp:sm:3";
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
local xmlns_delay = "urn:xmpp:delay";
local sm2_attr = { xmlns = xmlns_sm2 };
local sm3_attr = { xmlns = xmlns_sm3 };
local resume_timeout = module:get_option_number("smacks_hibernation_time", 300);
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 60);
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
local core_process_stanza = prosody.core_process_stanza;
local sessionmanager = require"core.sessionmanager";
local c2s_sessions = module:shared("/*/c2s/sessions");
local function init_session_cache(max_entries, evict_callback)
-- old prosody version < 0.10 (no limiting at all!)
if not cache then
local store = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
return store[key];
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
store[key] = value;
end;
};
end
-- use per user limited cache for prosody >= 0.10
local stores = {};
return {
get = function(user, key)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then
stores[user] = cache.new(max_entries, evict_callback);
end
return stores[user]:get(key);
end;
set = function(user, key, value)
if not user then return nil; end
if not key then return nil; end
if not stores[user] then stores[user] = cache.new(max_entries, evict_callback); end
stores[user]:set(key, value);
-- remove empty caches completely
if not stores[user]:count() then stores[user] = nil; end
end;
};
end
local old_session_registry = init_session_cache(max_old_sessions, nil);
local session_registry = init_session_cache(max_hibernated_sessions, function(resumption_token, session)
if session.destroyed then return true; end -- destroyed session can always be removed from cache
session.log("warn", "User has too much hibernated sessions, removing oldest session (token: %s)", resumption_token);
-- store old session's h values on force delete
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
return true; -- allow session to be removed from full cache to make room for new one
end);
local function stoppable_timer(delay, callback)
local stopped = false;
return {
stop = function () stopped = true end;
module:add_timer(delay, function (t)
if stopped then return; end
return callback(t);
end);
};
end
local function delayed_ack_function(session)
-- fire event only if configured to do so and our session is not hibernated or destroyed
if delayed_ack_timeout > 0 and session.awaiting_ack
and not session.hibernating and not session.destroyed then
session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d",
session.outgoing_stanza_queue and #session.outgoing_stanza_queue or 0);
module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue});
end
session.delayed_ack_timer = nil;
end
local function can_do_smacks(session, advertise_only)
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
local session_type = session.type;
if session.username then
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
end
return true;
elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then
return true;
end
return false, "service-unavailable", "Stream management is not available for this stream";
end
module:hook("stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
module:hook("s2s-stream-features",
function (event)
if can_do_smacks(event.origin, true) then
event.features:tag("sm", sm2_attr):tag("optional"):up():up();
event.features:tag("sm", sm3_attr):tag("optional"):up():up();
end
end);
local function request_ack_if_needed(session, force)
local queue = session.outgoing_stanza_queue;
if session.awaiting_ack == nil and not session.hibernating then
if (#queue > max_unacked_stanzas and session.last_queue_count ~= #queue) or force then
session.log("debug", "Queuing <r> (in a moment)");
session.awaiting_ack = false;
session.awaiting_ack_timer = stoppable_timer(1e-06, function ()
if not session.awaiting_ack and not session.hibernating then
session.log("debug", "Sending <r> (inside timer, before send)");
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
session.log("debug", "Sending <r> (inside timer, after send)");
session.awaiting_ack = true;
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
end
end
end);
end
end
-- Trigger "smacks-ack-delayed"-event if we added new (ackable) stanzas to the outgoing queue
-- and there isn't already a timer for this event running.
-- If we wouldn't do this, stanzas added to the queue after the first "smacks-ack-delayed"-event
-- would not trigger this event (again).
if #queue > max_unacked_stanzas and session.awaiting_ack and session.delayed_ack_timer == nil then
session.log("debug", "Calling delayed_ack_function directly (still waiting for ack)");
delayed_ack_function(session);
end
session.last_queue_count = #queue;
end
local function outgoing_stanza_filter(stanza, session)
local is_stanza = stanza.attr and not stanza.attr.xmlns and not stanza.name:find":";
if is_stanza and not stanza._cached then -- Stanza in default stream namespace
local queue = session.outgoing_stanza_queue;
local cached_stanza = st.clone(stanza);
cached_stanza._cached = true;
if cached_stanza and cached_stanza.name ~= "iq" and cached_stanza:get_child("delay", xmlns_delay) == nil then
cached_stanza = cached_stanza:tag("delay", {
xmlns = xmlns_delay,
from = jid.bare(session.full_jid or session.host),
stamp = datetime.datetime()
});
end
queue[#queue+1] = cached_stanza;
session.log("debug", "#queue = %d", #queue);
if session.hibernating then
session.log("debug", "hibernating, stanza queued");
return nil;
end
request_ack_if_needed(session, false);
end
return stanza;
end
local function count_incoming_stanzas(stanza, session)
if not stanza.attr.xmlns then
session.handled_stanza_count = session.handled_stanza_count + 1;
session.log("debug", "Handled %d incoming stanzas", session.handled_stanza_count);
end
return stanza;
end
local function wrap_session_out(session, resume)
if not resume then
session.outgoing_stanza_queue = {};
session.last_acknowledged_stanza = 0;
end
add_filter(session, "stanzas/out", outgoing_stanza_filter, -999);
local session_close = session.close;
function session.close(...)
if session.resumption_token then
session_registry.set(session.username, session.resumption_token, nil);
old_session_registry.set(session.username, session.resumption_token, nil);
session.resumption_token = nil;
end
-- send out last ack as per revision 1.5.2 of XEP-0198
if session.smacks and session.conn then
(session.sends2s or session.send)(st.stanza("a", { xmlns = session.smacks, h = string.format("%d", session.handled_stanza_count) ,t = tostring(os.time())) }));
end
return session_close(...);
end
return session;
end
local function wrap_session_in(session, resume)
if not resume then
session.handled_stanza_count = 0;
end
add_filter(session, "stanzas/in", count_incoming_stanzas, 999);
return session;
end
local function wrap_session(session, resume)
wrap_session_out(session, resume);
wrap_session_in(session, resume);
return session;
end
function handle_enable(session, stanza, xmlns_sm)
local ok, err, err_text = can_do_smacks(session);
if not ok then
session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it?
(session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors}));
return true;
end
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session(session, false);
local resume_token;
local resume = stanza.attr.resume;
if resume == "true" or resume == "1" then
resume_token = uuid_generate();
session_registry.set(session.username, resume_token, session);
session.resumption_token = resume_token;
end
(session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = tostring(resume_timeout) }));
return true;
end
module:hook_stanza(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100);
module:hook_stanza("http://etherx.jabber.org/streams", "features",
function (session, stanza)
stoppable_timer(1e-6, function ()
if can_do_smacks(session) then
if stanza:get_child("sm", xmlns_sm3) then
session.sends2s(st.stanza("enable", sm3_attr));
session.smacks = xmlns_sm3;
elseif stanza:get_child("sm", xmlns_sm2) then
session.sends2s(st.stanza("enable", sm2_attr));
session.smacks = xmlns_sm2;
else
return;
end
wrap_session_out(session, false);
end
end);
end);
function handle_enabled(session, stanza, xmlns_sm)
module:log("debug", "Enabling stream management");
session.smacks = xmlns_sm;
wrap_session_in(session, false);
-- FIXME Resume?
return true;
end
module:hook_stanza(xmlns_sm2, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm2); end, 100);
module:hook_stanza(xmlns_sm3, "enabled", function (session, stanza) return handle_enabled(session, stanza, xmlns_sm3); end, 100);
function handle_r(origin, stanza, xmlns_sm)
if not origin.smacks then
module:log("debug", "Received ack request from non-smack-enabled session");
return;
end
module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count);
-- Reply with <a>
-- ssa: add t with os.time
(origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = string.format("%d", origin.handled_stanza_count), t = tostring(os.time()) }));
return true;
end
module:hook_stanza(xmlns_sm2, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "r", function (origin, stanza) return handle_r(origin, stanza, xmlns_sm3); end);
function handle_a(origin, stanza)
if not origin.smacks then return; end
origin.awaiting_ack = nil;
if origin.awaiting_ack_timer then
origin.awaiting_ack_timer:stop();
end
if origin.delayed_ack_timer then
origin.delayed_ack_timer:stop();
origin.delayed_ack_timer = nil;
end
-- Remove handled stanzas from outgoing_stanza_queue
-- origin.log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or "");
local h = tonumber(stanza.attr.h);
if not h then
origin:close{ condition = "invalid-xml"; text = "Missing or invalid 'h' attribute"; };
end
local handled_stanza_count = h-origin.last_acknowledged_stanza;
local queue = origin.outgoing_stanza_queue;
if handled_stanza_count > #queue then
origin.log("warn", "The client says it handled %d new stanzas, but we only sent %d :)",
handled_stanza_count, #queue);
origin.log("debug", "Client h: %d, our h: %d", tonumber(stanza.attr.h), origin.last_acknowledged_stanza);
for i=1,#queue do
origin.log("debug", "Q item %d: %s", i, tostring(queue[i]));
end
end
for i=1,math_min(handled_stanza_count,#queue) do
t_remove(origin.outgoing_stanza_queue, 1);
end
origin.log("debug", "#queue = %d", #queue);
origin.last_acknowledged_stanza = origin.last_acknowledged_stanza + handled_stanza_count;
request_ack_if_needed(origin, false)
return true;
end
module:hook_stanza(xmlns_sm2, "a", handle_a);
module:hook_stanza(xmlns_sm3, "a", handle_a);
--TODO: Optimise... incoming stanzas should be handled by a per-session
-- function that has a counter as an upvalue (no table indexing for increments,
-- and won't slow non-198 sessions). We can also then remove the .handled flag
-- on stanzas
function handle_unacked_stanzas(session)
local queue = session.outgoing_stanza_queue;
local error_attr = { type = "cancel" };
if #queue > 0 then
session.outgoing_stanza_queue = {};
for i=1,#queue do
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
end
end
end
end
module:hook("pre-resource-unbind", function (event)
local session, err = event.session, event.error;
if session.smacks then
if not session.resumption_token then
local queue = session.outgoing_stanza_queue;
if #queue > 0 then
session.log("debug", "Destroying session with %d unacked stanzas", #queue);
handle_unacked_stanzas(session);
end
else
session.log("debug", "mod_smacks hibernating session for up to %d seconds", resume_timeout);
local hibernate_time = os_time(); -- Track the time we went into hibernation
session.hibernating = hibernate_time;
local resumption_token = session.resumption_token;
module:fire_event("smacks-hibernation-start", {origin = session, queue = session.outgoing_stanza_queue});
timer.add_task(resume_timeout, function ()
session.log("debug", "mod_smacks hibernation timeout reached...");
-- We need to check the current resumption token for this resource
-- matches the smacks session this timer is for in case it changed
-- (for example, the client may have bound a new resource and
-- started a new smacks session, or not be using smacks)
local curr_session = full_sessions[session.full_jid];
if session.destroyed then
session.log("debug", "The session has already been destroyed");
elseif curr_session and curr_session.resumption_token == resumption_token
-- Check the hibernate time still matches what we think it is,
-- otherwise the session resumed and re-hibernated.
and session.hibernating == hibernate_time then
-- wait longer if the timeout isn't reached because push was enabled for this session
-- session.first_hibernated_push is the starting point for hibernation timeouts of those push enabled clients
-- wait for an additional resume_timeout seconds if no push occured since hibernation at all
local current_time = os_time();
local timeout_start = math_max(session.hibernating, session.first_hibernated_push or session.hibernating);
if session.push_identifier ~= nil and not session.first_hibernated_push then
session.log("debug", "No push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout);
return resume_timeout;
end
if current_time-timeout_start < resume_timeout and session.push_identifier ~= nil then
session.log("debug", "A push happened since hibernation started, hibernating session for up to %d extra seconds", current_time-timeout_start);
return current_time-timeout_start; -- time left to wait
end
session.log("debug", "Destroying session for hibernating too long");
session_registry.set(session.username, session.resumption_token, nil);
-- save only actual h value and username/host (for security)
old_session_registry.set(session.username, session.resumption_token, {
h = session.handled_stanza_count,
username = session.username,
host = session.host
});
session.resumption_token = nil;
sessionmanager.destroy_session(session);
else
session.log("debug", "Session resumed before hibernation timeout, all is well")
end
end);
return true; -- Postpone destruction for now
end
end
end);
local function handle_s2s_destroyed(event)
local session = event.session;
local queue = session.outgoing_stanza_queue;
if queue and #queue > 0 then
session.log("warn", "Destroying session with %d unacked stanzas", #queue);
if s2s_resend then
for i = 1, #queue do
module:send(queue[i]);
end
session.outgoing_stanza_queue = nil;
else
handle_unacked_stanzas(session);
end
end
end
module:hook("s2sout-destroyed", handle_s2s_destroyed);
module:hook("s2sin-destroyed", handle_s2s_destroyed);
local function get_session_id(session)
return session.id or (tostring(session):match("[a-f0-9]+$"));
end
function handle_resume(session, stanza, xmlns_sm)
if session.full_jid then
session.log("warn", "Tried to resume after resource binding");
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("unexpected-request", { xmlns = xmlns_errors })
);
return true;
end
local id = stanza.attr.previd;
local original_session = session_registry.get(session.username, id);
if not original_session then
session.log("debug", "Tried to resume non-existent session with id %s", id);
local old_session = old_session_registry.get(session.username, id);
if old_session and session.username == old_session.username
and session.host == old_session.host
and old_session.h then
session.send(st.stanza("failed", { xmlns = xmlns_sm, h = string.format("%d", old_session.h) })
:tag("item-not-found", { xmlns = xmlns_errors })
);
else
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("item-not-found", { xmlns = xmlns_errors })
);
end;
elseif session.username == original_session.username
and session.host == original_session.host then
session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session));
-- TODO: All this should move to sessionmanager (e.g. session:replace(new_session))
if original_session.conn then
original_session.log("debug", "mod_smacks closing an old connection for this session");
local conn = original_session.conn;
c2s_sessions[conn] = nil;
conn:close();
end
original_session.ip = session.ip;
original_session.conn = session.conn;
original_session.send = session.send;
original_session.close = session.close;
original_session.filter = session.filter;
original_session.filter.session = original_session;
original_session.filters = session.filters;
original_session.stream = session.stream;
original_session.secure = session.secure;
original_session.hibernating = nil;
wrap_session(original_session, true);
-- Inform xmppstream of the new session (passed to its callbacks)
original_session.stream:set_session(original_session);
-- Similar for connlisteners
c2s_sessions[session.conn] = original_session;
original_session.send(st.stanza("resumed", { xmlns = xmlns_sm,
h = string.format("%d", original_session.handled_stanza_count), previd = id }));
-- Fake an <a> with the h of the <resume/> from the client
original_session:dispatch_stanza(st.stanza("a", { xmlns = xmlns_sm,
h = stanza.attr.h }));
-- 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
local queue = original_session.outgoing_stanza_queue;
original_session.log("debug", "#queue = %d", #queue);
for i=1,#queue do
original_session.send(queue[i]);
end
original_session.log("debug", "#queue = %d -- after send", #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;
end
module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue});
request_ack_if_needed(original_session, true);
else
module:log("warn", "Client %s@%s[%s] tried to resume stream for %s@%s[%s]",
session.username or "?", session.host or "?", session.type,
original_session.username or "?", original_session.host or "?", original_session.type);
session.send(st.stanza("failed", { xmlns = xmlns_sm })
:tag("not-authorized", { xmlns = xmlns_errors }));
end
return true;
end
module:hook_stanza(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end);
module:hook_stanza(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end);
local function handle_read_timeout(event)
local session = event.session;
if session.smacks then
if session.awaiting_ack then
if session.awaiting_ack_timer then
session.awaiting_ack_timer:stop();
end
if session.delayed_ack_timer then
session.delayed_ack_timer:stop();
session.delayed_ack_timer = nil;
end
return false; -- Kick the session
end
session.log("debug", "Sending <r> (read timeout)");
session.awaiting_ack = false;
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
session.awaiting_ack = true;
if not session.delayed_ack_timer then
session.delayed_ack_timer = stoppable_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
end
return true;
end
end
module:hook("s2s-read-timeout", handle_read_timeout);
module:hook("c2s-read-timeout", handle_read_timeout);
@@ -1,4 +0,0 @@
-- this module is deprecated, log an error and load the superseding module instead
module:depends"smacks"
module:log("info", "mod_smacks_offline is no lobger required.");
+5 -4
View File
@@ -7,6 +7,7 @@
--
local http = require "net.http";
local storagemanager = require "core.storagemanager";
local json = require "util.json";
-- local ltn12 = require "ltn12"
local jid_bare = require "util.jid".bare;
@@ -39,10 +40,10 @@ local vcard_storage = module:open_store("vcard");
local private_storage = module:open_store("private");
timer.add_task(3, function ()
private_storage = module:open_host_store(storage_host, "private");
vcard_storage = module:open_host_store(storage_host, "vcard");
fcm_token_store = module:open_host_store(storage_host, "fcmtoken");
fcm_token_map_store = module:open_host_store(storage_host, "fcmtoken", "map");
private_storage = storagemanager.open(storage_host, "private");
vcard_storage = storagemanager.open(storage_host, "vcard");
fcm_token_store = storagemanager.open(storage_host, "fcmtoken");
fcm_token_map_store = storagemanager.open(storage_host, "fcmtoken", "map");
if not private_storage or not vcard_storage then
module:log("info", "private/vcard_store %s/%s not found? - will try again", private_storage, vcard_storage);
return 3;
+5 -4
View File
@@ -7,6 +7,7 @@
--
local http = require "net.http";
local storagemanager = require "core.storagemanager";
local json = require "util.json";
-- local ltn12 = require "ltn12"
local jid_bare = require "util.jid".bare;
@@ -38,10 +39,10 @@ local vcard_storage = module:open_store("vcard");
local private_storage = module:open_store("private");
timer.add_task(3, function ()
private_storage = module:open_host_store(storage_host, "private");
vcard_storage = module:open_host_store(storage_host, "vcard");
fcm_token_store = module:open_host_store(storage_host, "fcmtoken");
fcm_token_map_store = module:open_host_store(storage_host, "fcmtoken", "map");
private_storage = storagemanager.open(storage_host, "private");
vcard_storage = storagemanager.open(storage_host, "vcard");
fcm_token_store = storagemanager.open(storage_host, "fcmtoken");
fcm_token_map_store = storagemanager.open(storage_host, "fcmtoken", "map");
if not private_storage or not vcard_storage then
module:log("info", "private/vcard_store %s/%s not found? - will try again", private_storage, vcard_storage);
return 3;