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:
@@ -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
@@ -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/ && \
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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,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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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.");
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user