diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9ab3dfe62..f08d71093 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,13 +29,15 @@ jobs: - name: Install prosody run: | + echo deb http://packages.prosody.im/debian $(lsb_release -sc) main | sudo tee /etc/apt/sources.list.d/prosody.list + sudo wget https://prosody.im/files/prosody-debian-packages.key -O/etc/apt/trusted.gpg.d/prosody.gpg sudo apt-get update sudo apt-get -y install prosody lua-bitop lua-sec sudo service prosody stop - run: npm install -g npm - run: make - - run: make test-ci + - run: make ci - name: The job has failed if: ${{ failure() }} diff --git a/.gitignore b/.gitignore index 67d9fb3dc..a942020ea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,10 @@ node_modules/ **/dist/* server/localhost/ -server/localhost.crt -server/localhost.key +server/certs/ server/prosody.err server/prosody.log server/prosody.pid -server/prosody-modules/ !.gitkeep !.editorconfig diff --git a/.husky/pre-push b/.husky/pre-push index 07792bbf8..dc870ce70 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -4,7 +4,7 @@ BRANCH="$(git rev-parse --abbrev-ref HEAD)" if [[ "$BRANCH" == "main" ]]; then - make test-ci + make ci else make test fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e37fd6607..d35d5e8d5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,10 +29,10 @@ See [ava CLI](https://github.com/avajs/ava/blob/main/docs/05-command-line.md). ## Submitting When submitting a pull request, additional tests will be run on GitHub actions. -In most cases it shouldn't be necessary but if they fail, you can run them locally after installing prosody with +In most cases it shouldn't be necessary but if they fail, you can run them locally after installing prosody >= 0.12 with ``` -make test-ci +make ci ``` Good luck and feel free to ask for help in https://github.com/xmppjs/xmpp.js/discussions diff --git a/Makefile b/Makefile index 2b7b36425..cce6c915f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: setup test clean bundle start stop restart size bundlesize +.PHONY: setup lint test ci clean start stop restart bundlesize bundle size cert ncu setup: node packages/xmpp.js/script.js @@ -19,7 +19,7 @@ test: make lint make bundlesize -test-ci: +ci: npm install ./node_modules/.bin/lerna bootstrap ./node_modules/.bin/ava @@ -27,7 +27,7 @@ test-ci: make restart ./node_modules/.bin/lerna run prepublish node bundle.js - ./node_modules/.bin/ava --config e2e.config.js + ./node_modules/.bin/ava --tap --config e2e.config.js make bundlesize clean: @@ -61,8 +61,5 @@ size: make bundle make bundlesize -cert: - cd server && openssl req -new -x509 -days 365 -nodes -out "localhost.crt" -newkey rsa:2048 -keyout "localhost.key" -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" - ncu: ncu && npx lerna exec ncu diff --git a/README.md b/README.md index 7b3ebf5a2..43bf8e727 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ It aims to run everywhere JavaScript runs and make use of the best network transport available. -xmpp.js is known to be used in Node.js, browsers, React Native, GJS and Duktape. +xmpp.js is known to be used with Node.js, browsers, React Native, GJS and Duktape. ### reliable @@ -45,8 +45,11 @@ Do you need help with working with xmpp.js? Please reach out to our community by ## Built with xmpp.js +- [WorkAdventure](https://workadventu.re/) - [Simplo](https://simplo.app/?lang=en) - [Crypho](https://www.crypho.com/) +- [MeshCentral](https://meshcentral.com/) +- [openHAB](https://www.openhab.org/) - [HearMe.App](https://www.hearme.app/) - [Wobbly](https://wobbly.app/) - [Alcatel-Lucent Rainbow](https://www.openrainbow.com/) diff --git a/server/certs/.gitkeep b/server/certs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/server/index.js b/server/index.js index c9f38fd4a..e7fe82905 100644 --- a/server/index.js +++ b/server/index.js @@ -47,8 +47,8 @@ function makeCertificate() { const attrs = [{ name: "commonName", value: "localhost" }]; const pems = selfsigned.generate(attrs, { days: 365, keySize: 2048 }); - writeFileSync(path.join(__dirname, "localhost.crt"), pems.cert); - writeFileSync(path.join(__dirname, "localhost.key"), pems.private); + writeFileSync(path.join(__dirname, "certs/localhost.crt"), pems.cert); + writeFileSync(path.join(__dirname, "certs/localhost.key"), pems.private); } async function waitPortClose() { diff --git a/server/localhost/certs/.gitkeep b/server/localhost/certs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/server/prosody-modules/mod_smacks/README.markdown b/server/prosody-modules/mod_smacks/README.markdown deleted file mode 100644 index 4542770a0..000000000 --- a/server/prosody-modules/mod_smacks/README.markdown +++ /dev/null @@ -1,85 +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. - -If you don't want this behaviour on timeout you can use [mod_smacks_offline] -or [mod_smacks_noerror] to customize the behaviour further. - -This module also provides some events used by [mod_cloud_notify]. -These events are: "smacks-ack-delayed", "smacks-hibernation-start" and -"smacks-hibernation-end". See [mod_cloud_notify] for details on how this -events are used there. - -Use prosody 0.10+ to have per user limits on allowed sessions in hibernation -state and allowed sessions for which the h-value is kept even after the -hibernation timed out. -These are settable using `smacks_max_hibernated_sessions` and `smacks_max_old_sessions`. - -# 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 -`smacks_max_ack_delay` 60 (1 minute) The number of seconds an ack must be unanswered to trigger an "smacks-ack-delayed" event -`smacks_max_hibernated_sessions` 10 The number of allowed sessions in hibernated state (limited per user) -`smacks_max_old_sessions` 10 The number of allowed sessions with timed out hibernation for which the h-value is still kept (limited per user) - -# Compatibility - ---- - -0.10 Works -0.9 Works, no per user limit of hibernated sessions -0.8 Works, no per user limit of hibernated sessions, use version [7693724881b3] - ---- - -# Clients - -Clients that support [XEP-0198]: - -- Gajim (Linux, Windows, OS X) -- Conversations (Android) -- ChatSecure (iOS) -- Swift (but not resumption, as of version 2.0 and alphas of 3.0) -- Psi (in an unreleased branch) -- Yaxim (Android) -- Monal (iOS) - -[7693724881b3]: //hg.prosody.im/prosody-modules/raw-file/7693724881b3/mod_smacks/mod_smacks.lua -[mod_smacks_offline]: //modules.prosody.im/mod_smacks_offline -[mod_smacks_noerror]: //modules.prosody.im/mod_smacks_noerror -[mod_cloud_notify]: //modules.prosody.im/mod_cloud_notify diff --git a/server/prosody-modules/mod_smacks/mod_smacks.lua b/server/prosody-modules/mod_smacks/mod_smacks.lua deleted file mode 100644 index 3c7add7e8..000000000 --- a/server/prosody-modules/mod_smacks/mod_smacks.lua +++ /dev/null @@ -1,611 +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-2019 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"; - -assert(max_hibernated_sessions > 0, "smacks_max_hibernated_sessions must be greater than 0"); -assert(max_old_sessions > 0, "smacks_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 () 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 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 (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 (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 (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) - 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; - 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 - (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 - -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 - 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 - -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 - 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 with the h of the 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, "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 (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); diff --git a/server/prosody.cfg.lua b/server/prosody.cfg.lua index 1fb5c4d1b..480c0021a 100644 --- a/server/prosody.cfg.lua +++ b/server/prosody.cfg.lua @@ -4,8 +4,6 @@ local lfs = require "lfs"; -plugin_paths = { lfs.currentdir() .. "/prosody-modules" } - modules_enabled = { "roster"; "saslauth"; @@ -19,10 +17,7 @@ modules_enabled = { "websocket"; "time"; "version"; - - -- prosody-modules "smacks"; - -- "smacks_offline"; }; modules_disabled = { @@ -44,7 +39,7 @@ cross_domain_websocket = true; authentication = "internal_plain" -legacy_ssl_ports = { 5223 }; +c2s_direct_tls_ports = { 5223 }; log = { debug = lfs.currentdir() .. "/prosody.log"; @@ -52,8 +47,8 @@ log = { } ssl = { - certificate = lfs.currentdir() .. "/localhost.crt"; - key = lfs.currentdir() .. "/localhost.key"; + certificate = lfs.currentdir() .. "/certs/localhost.crt"; + key = lfs.currentdir() .. "/certs/localhost.key"; } data_path = lfs.currentdir() diff --git a/test/client.js b/test/client.js index 82bcccb6c..a4790f45e 100644 --- a/test/client.js +++ b/test/client.js @@ -182,8 +182,7 @@ test.serial("auto", async (t) => { t.is(address.bare().toString(), JID); }); -// Prosody 404 https://prosody.im/issues/issue/932 -test.serial.skip("ws IPv4", async (t) => { +test.serial("ws IPv4", async (t) => { const xmpp = client({ credentials, service: "ws://127.0.0.1:5280/xmpp-websocket", @@ -195,8 +194,7 @@ test.serial.skip("ws IPv4", async (t) => { t.is(address.bare().toString(), JID); }); -// Prosody 404 https://prosody.im/issues/issue/932 -test.serial.skip("ws IPv6", async (t) => { +test.serial("ws IPv6", async (t) => { const xmpp = client({ credentials, service: "ws://[::1]:5280/xmpp-websocket", @@ -220,7 +218,7 @@ test.serial("ws domain", async (t) => { }); // Prosody 404 https://prosody.im/issues/issue/932 -test.serial.skip("wss IPv4", async (t) => { +test.serial("wss IPv4", async (t) => { const xmpp = client({ credentials, service: "wss://127.0.0.1:5281/xmpp-websocket", @@ -233,7 +231,7 @@ test.serial.skip("wss IPv4", async (t) => { }); // Prosody 404 https://prosody.im/issues/issue/932 -test.serial.skip("wss IPv6", async (t) => { +test.serial("wss IPv6", async (t) => { const xmpp = client({ credentials, service: "wss://[::1]:5281/xmpp-websocket", @@ -272,11 +270,6 @@ test.serial("xmpp IPv6", async (t) => { const xmpp = client({ credentials, service: "xmpp://[::1]:5222", domain }); debug(xmpp); t.context.xmpp = xmpp; - // No local IPv6 on travis https://github.com/travis-ci/travis-ci/issues/4964 - if (process.env.TRAVIS) { - return t.pass(); - } - const address = await xmpp.start(); t.is(address.bare().toString(), JID); }); @@ -305,11 +298,6 @@ test.serial("xmpps IPv6", async (t) => { const xmpp = client({ credentials, service: "xmpps://[::1]:5223", domain }); debug(xmpp); t.context.xmpp = xmpp; - // No local IPv6 on travis https://github.com/travis-ci/travis-ci/issues/4964 - if (process.env.TRAVIS) { - return t.pass(); - } - const address = await xmpp.start(); t.is(address.bare().toString(), JID); });