aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.luacheckrc5
-rw-r--r--GNUmakefile7
-rw-r--r--TODO1
-rwxr-xr-xconfigure56
-rw-r--r--core/moduleapi.lua83
-rw-r--r--core/rostermanager.lua28
-rw-r--r--core/s2smanager.lua3
-rw-r--r--core/statsmanager.lua1
-rw-r--r--makefile10
-rw-r--r--net/adns.lua16
-rw-r--r--net/connlisteners.lua18
-rw-r--r--net/resolvers/basic.lua1
-rw-r--r--net/resolvers/manual.lua1
-rw-r--r--net/resolvers/service.lua1
-rw-r--r--net/server_epoll.lua44
-rw-r--r--net/server_event.lua14
-rw-r--r--net/server_select.lua65
-rw-r--r--net/websocket/frames.lua7
-rw-r--r--plugins/mod_admin_telnet.lua72
-rw-r--r--plugins/mod_bosh.lua29
-rw-r--r--plugins/mod_c2s.lua10
-rw-r--r--plugins/mod_component.lua2
-rw-r--r--plugins/mod_http.lua33
-rw-r--r--plugins/mod_http_errors.lua25
-rw-r--r--plugins/mod_mam/mod_mam.lua77
-rw-r--r--plugins/mod_pep.lua17
-rw-r--r--plugins/mod_pep_simple.lua1
-rw-r--r--plugins/mod_presence.lua12
-rw-r--r--plugins/mod_pubsub/mod_pubsub.lua7
-rw-r--r--plugins/mod_s2s/mod_s2s.lua3
-rw-r--r--plugins/mod_saslauth.lua3
-rw-r--r--plugins/mod_storage_memory.lua8
-rw-r--r--plugins/mod_storage_sql.lua2
-rw-r--r--plugins/mod_tls.lua24
-rw-r--r--plugins/mod_websocket.lua42
-rw-r--r--plugins/muc/mod_muc.lua2
-rw-r--r--plugins/muc/muc.lib.lua78
-rw-r--r--plugins/muc/subject.lib.lua6
-rwxr-xr-xprosodyctl17
-rw-r--r--spec/core_storagemanager_spec.lua2
-rw-r--r--spec/scansion/keep_full_sub_req.scs58
-rw-r--r--spec/scansion/muc_subject_issue_667.scs129
-rw-r--r--spec/scansion/prosody.cfg.lua10
-rw-r--r--spec/util_format_spec.lua3
-rw-r--r--spec/util_http_spec.lua5
-rw-r--r--spec/util_interpolation_spec.lua17
-rw-r--r--spec/util_stanza_spec.lua19
-rw-r--r--spec/util_table_spec.lua17
-rw-r--r--util-src/pposix.c4
-rw-r--r--util-src/time.c2
-rw-r--r--util/error.lua52
-rw-r--r--util/format.lua18
-rw-r--r--util/http.lua22
-rw-r--r--util/import.lua2
-rw-r--r--util/iterators.lua6
-rw-r--r--util/multitable.lua2
-rw-r--r--util/promise.lua3
-rw-r--r--util/prosodyctl.lua7
-rw-r--r--util/serialization.lua1
-rw-r--r--util/stanza.lua8
60 files changed, 910 insertions, 308 deletions
diff --git a/.luacheckrc b/.luacheckrc
index ce3d377b..b2fa7cdb 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -1,7 +1,8 @@
cache = true
codes = true
-ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "143/table", "113/unpack" }
+ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", }
+std = "lua53c"
max_line_length = 150
read_globals = {
@@ -79,6 +80,7 @@ files["plugins/"] = {
"module.remove_item",
"module.require",
"module.send",
+ "module.send_iq",
"module.set_global",
"module.shared",
"module.unhook",
@@ -131,7 +133,6 @@ if os.getenv("PROSODY_STRICT_LINT") ~= "1" then
"fallbacks/bit.lua";
"fallbacks/lxp.lua";
- "net/adns.lua";
"net/cqueues.lua";
"net/dns.lua";
"net/server_select.lua";
diff --git a/GNUmakefile b/GNUmakefile
index 6c134679..977e91c6 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -21,6 +21,7 @@ MKDIR_PRIVATE=$(MKDIR) -m750
LUACHECK=luacheck
BUSTED=busted
+SCANSION=scansion
.PHONY: all test coverage clean install
@@ -71,6 +72,12 @@ clean:
test:
$(BUSTED) --lua=$(RUNWITH)
+integration-test: all
+ $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua start
+ $(SCANSION) -d ./spec/scansion; R=$$? \
+ $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua stop \
+ exit $$R
+
coverage:
-rm -- luacov.*
$(BUSTED) --lua=$(RUNWITH) -c
diff --git a/TODO b/TODO
index d22d20f4..9ec820b2 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1,4 @@
== 1.0 ==
- Roster providers
-- Statistics
- Clustering
- World domination
diff --git a/configure b/configure
index 4307997c..dec7b60d 100755
--- a/configure
+++ b/configure
@@ -237,7 +237,7 @@ do
--lua-version|--with-lua-version)
[ -n "$value" ] || die "Missing value in flag $key."
LUA_VERSION="$value"
- [ "$LUA_VERSION" = "5.1" ] || [ "$LUA_VERSION" = "5.2" ] || [ "$LUA_VERSION" = "5.3" ] || die "Invalid Lua version in flag $key."
+ [ "$LUA_VERSION" = "5.1" ] || [ "$LUA_VERSION" = "5.2" ] || [ "$LUA_VERSION" = "5.3" ] || [ "$LUA_VERSION" = "5.4" ] || die "Invalid Lua version in flag $key."
LUA_VERSION_SET=yes
;;
--with-lua)
@@ -340,7 +340,7 @@ then
fi
detect_lua_version() {
- detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[123])$"))' 2> /dev/null)
+ detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[1234])$"))' 2> /dev/null)
if [ "$detected_lua" != "nil" ]
then
if [ "$LUA_VERSION_SET" != "yes" ]
@@ -403,8 +403,14 @@ then
elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.3" ]
then
suffixes="5.3 53 -5.3 -53"
+ elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.4" ]
+ then
+ suffixes="5.4 54 -5.4 -54"
else
- suffixes="5.1 51 -5.1 -51 5.2 52 -5.2 -52 5.3 53 -5.3 -53"
+ suffixes="5.1 51 -5.1 -51"
+ suffixes="$suffixes 5.2 52 -5.2 -52"
+ suffixes="$suffixes 5.3 53 -5.3 -53"
+ suffixes="$suffixes 5.4 54 -5.4 -54"
fi
for suffix in "" $suffixes
do
@@ -457,30 +463,46 @@ then
LUA_LIBDIR="$LUA_DIR/lib"
fi
-echo_n "Checking Lua includes... "
lua_h="$LUA_INCDIR/lua.h"
+echo_n "Looking for lua.h at $lua_h..."
if [ -f "$lua_h" ]
then
- echo "lua.h found in $lua_h"
+ echo found
else
- v_dir="$LUA_INCDIR/lua/$LUA_VERSION"
- lua_h="$v_dir/lua.h"
- if [ -f "$lua_h" ]
- then
- echo "lua.h found in $lua_h"
+ echo "not found"
+ for postfix in "$LUA_VERSION" "$LUA_SUFFIX"; do
+ if ! [ "$postfix" = "" ]; then
+ v_dir="$LUA_INCDIR/lua/$postfix";
+ else
+ v_dir="$LUA_INCDIR/lua";
+ fi
+ lua_h="$v_dir/lua.h"
+ echo_n "Looking for lua.h at $lua_h..."
+ if [ -f "$lua_h" ]
+ then
LUA_INCDIR="$v_dir"
- else
- d_dir="$LUA_INCDIR/lua$LUA_VERSION"
+ echo found
+ break;
+ else
+ echo "not found"
+ d_dir="$LUA_INCDIR/lua$postfix"
lua_h="$d_dir/lua.h"
+ echo_n "Looking for lua.h at $lua_h..."
if [ -f "$lua_h" ]
then
- echo "lua.h found in $lua_h (Debian/Ubuntu)"
- LUA_INCDIR="$d_dir"
+ echo found
+ LUA_INCDIR="$d_dir"
+ break;
else
- echo "lua.h not found (looked in $LUA_INCDIR, $v_dir, $d_dir)"
- die "You may want to use the flag --with-lua or --with-lua-include. See --help."
+ echo "not found"
fi
- fi
+ fi
+ done
+ if [ ! -f "$lua_h" ]; then
+ echo "lua.h not found."
+ echo
+ die "You may want to use the flag --with-lua or --with-lua-include. See --help."
+ fi
fi
if [ "$lua_interp_found" = "yes" ]
diff --git a/core/moduleapi.lua b/core/moduleapi.lua
index 10f9f04d..c6193cfd 100644
--- a/core/moduleapi.lua
+++ b/core/moduleapi.lua
@@ -14,13 +14,16 @@ local pluginloader = require "util.pluginloader";
local timer = require "util.timer";
local resolve_relative_path = require"util.paths".resolve_relative_path;
local st = require "util.stanza";
+local cache = require "util.cache";
+local errutil = require "util.error";
+local promise = require "util.promise";
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
local error, setmetatable, type = error, setmetatable, type;
local ipairs, pairs, select = ipairs, pairs, select;
local tonumber, tostring = tonumber, tostring;
local require = require;
-local pack = table.pack or function(...) return {n=select("#",...), ...}; end -- table.pack is only in 5.2
+local pack = table.pack or require "util.table".pack; -- table.pack is only in 5.2
local unpack = table.unpack or unpack; --luacheck: ignore 113 -- renamed in 5.2
local prosody = prosody;
@@ -361,6 +364,84 @@ function api:send(stanza, origin)
return core_post_stanza(origin or hosts[self.host], stanza);
end
+function api:send_iq(stanza, origin, timeout)
+ local iq_cache = self._iq_cache;
+ if not iq_cache then
+ iq_cache = cache.new(256, function (_, iq)
+ iq.reject(errutil.new({
+ type = "wait", condition = "resource-constraint",
+ text = "evicted from iq tracking cache"
+ }));
+ self:unhook(iq.result_event, iq.result_handler);
+ self:unhook(iq.error_event, iq.error_handler);
+ end);
+ self._iq_cache = iq_cache;
+ end
+ return promise.new(function (resolve, reject)
+ local event_type;
+ if stanza.attr.from == self.host then
+ event_type = "host";
+ else -- assume bare since we can't hook full jids
+ event_type = "bare";
+ end
+ local result_event = "iq-result/"..event_type.."/"..stanza.attr.id;
+ local error_event = "iq-error/"..event_type.."/"..stanza.attr.id;
+ local cache_key = event_type.."/"..stanza.attr.id;
+
+ local function result_handler(event)
+ if event.stanza.attr.from == stanza.attr.to then
+ resolve(event);
+ return true;
+ end
+ end
+
+ local function error_handler(event)
+ if event.stanza.attr.from == stanza.attr.to then
+ reject(errutil.from_stanza(event.stanza), event);
+ return true;
+ end
+ end
+
+ if iq_cache:get(cache_key) then
+ reject(errutil.new({
+ type = "modify", condition = "conflict",
+ text = "iq stanza id attribute already used",
+ }));
+ return;
+ end
+
+ self:hook(result_event, result_handler);
+ self:hook(error_event, error_handler);
+
+ local timeout_handle = self:add_timer(timeout or 120, function ()
+ reject(errutil.new({
+ type = "wait", condition = "remote-server-timeout",
+ text = "IQ stanza timed out",
+ }));
+ self:unhook(result_event, result_handler);
+ self:unhook(error_event, error_handler);
+ iq_cache:set(cache_key, nil);
+ end);
+
+ local ok = iq_cache:set(cache_key, {
+ reject = reject, resolve = resolve,
+ timeout_handle = timeout_handle,
+ result_event = result_event, error_event = error_event,
+ result_handler = result_handler, error_handler = error_handler;
+ });
+
+ if not ok then
+ reject(errutil.new({
+ type = "wait", condition = "internal-server-error",
+ text = "Could not store IQ tracking data"
+ }));
+ return;
+ end
+
+ self:send(stanza, origin);
+ end);
+end
+
function api:broadcast(jids, stanza, iter)
for jid in (iter or it.values)(jids) do
local new_stanza = st.clone(stanza);
diff --git a/core/rostermanager.lua b/core/rostermanager.lua
index 61b08002..d551a1b1 100644
--- a/core/rostermanager.lua
+++ b/core/rostermanager.lua
@@ -12,6 +12,7 @@
local log = require "util.logger".init("rostermanager");
local new_id = require "util.id".short;
+local new_cache = require "util.cache".new;
local pairs = pairs;
local tostring = tostring;
@@ -111,6 +112,23 @@ local function load_roster(username, host)
else -- Attempt to load roster for non-loaded user
log("debug", "load_roster: loading for offline user: %s", jid);
end
+ local roster_cache = hosts[host] and hosts[host].roster_cache;
+ if not roster_cache then
+ if hosts[host] then
+ roster_cache = new_cache(1024);
+ hosts[host].roster_cache = roster_cache;
+ end
+ else
+ roster = roster_cache:get(jid);
+ if roster then
+ log("debug", "load_roster: cache hit");
+ roster_cache:set(jid, roster);
+ if user then user.roster = roster; end
+ return roster;
+ else
+ log("debug", "load_roster: cache miss, loading from storage");
+ end
+ end
local roster_store = storagemanager.open(host, "roster", "keyval");
local data, err = roster_store:get(username);
roster = data or {};
@@ -134,6 +152,10 @@ local function load_roster(username, host)
if not err then
hosts[host].events.fire_event("roster-load", { username = username, host = host, roster = roster });
end
+ if roster_cache and not user then
+ log("debug", "load_roster: caching loaded roster");
+ roster_cache:set(jid, roster);
+ end
return roster, err;
end
@@ -263,15 +285,15 @@ end
function is_contact_pending_in(username, host, jid)
local roster = load_roster(username, host);
- return roster[false].pending[jid];
+ return roster[false].pending[jid] ~= nil;
end
-local function set_contact_pending_in(username, host, jid)
+local function set_contact_pending_in(username, host, jid, stanza)
local roster = load_roster(username, host);
local item = roster[jid];
if item and (item.subscription == "from" or item.subscription == "both") then
return; -- false
end
- roster[false].pending[jid] = true;
+ roster[false].pending[jid] = st.is_stanza(stanza) and st.preserialize(stanza) or true;
return save_roster(username, host, roster, jid);
end
function is_contact_pending_out(username, host, jid)
diff --git a/core/s2smanager.lua b/core/s2smanager.lua
index 58269c49..0ba5e7c6 100644
--- a/core/s2smanager.lua
+++ b/core/s2smanager.lua
@@ -50,6 +50,9 @@ local resting_session = { -- Resting, not dead
close = function (session)
session.log("debug", "Attempt to close already-closed session");
end;
+ reset_stream = function (session)
+ session.log("debug", "Attempt to reset stream of already-closed session");
+ end;
filter = function (type, data) return data; end; --luacheck: ignore 212/type
}; resting_session.__index = resting_session;
diff --git a/core/statsmanager.lua b/core/statsmanager.lua
index 237b1dd5..50798ad0 100644
--- a/core/statsmanager.lua
+++ b/core/statsmanager.lua
@@ -97,6 +97,7 @@ if stats then
end
timer.add_task(stats_interval, collect);
prosody.events.add_handler("server-started", function () collect() end, -1);
+ prosody.events.add_handler("server-stopped", function () collect() end, -1);
else
log("debug", "Statistics enabled using %s provider, collection is disabled", stats_provider_name);
end
diff --git a/makefile b/makefile
index d19ec24d..0b1c8788 100644
--- a/makefile
+++ b/makefile
@@ -19,6 +19,9 @@ INSTALL_EXEC=$(INSTALL) -m755
MKDIR=install -d
MKDIR_PRIVATE=$(MKDIR) -m750
+LUACHECK=luacheck
+BUSTED=busted
+
.PHONY: all test clean install
all: prosody.install prosodyctl.install prosody.cfg.lua.install prosody.version
@@ -68,8 +71,13 @@ clean:
rm -f prosody.version
$(MAKE) clean -C util-src
+lint:
+ $(LUACHECK) -q $$(HGPLAIN= hg files -I '**.lua') prosody prosodyctl
+ @echo $$(sed -n '/^\tlocal exclude_files/,/^}/p;' .luacheckrc | sed '1d;$d' | wc -l) files ignored
+ shellcheck configure
+
test:
- busted --lua=$(RUNWITH)
+ $(BUSTED) --lua=$(RUNWITH)
prosody.install: prosody
diff --git a/net/adns.lua b/net/adns.lua
index 560e4b53..4fa01f8a 100644
--- a/net/adns.lua
+++ b/net/adns.lua
@@ -14,7 +14,7 @@ local log = require "util.logger".init("adns");
local coroutine, tostring, pcall = coroutine, tostring, pcall;
local setmetatable = setmetatable;
-local function dummy_send(sock, data, i, j) return (j-i)+1; end
+local function dummy_send(sock, data, i, j) return (j-i)+1; end -- luacheck: ignore 212
local _ENV = nil;
-- luacheck: std none
@@ -29,8 +29,7 @@ local function new_async_socket(sock, resolver)
local peername = "<unknown>";
local listener = {};
local handler = {};
- local err;
- function listener.onincoming(conn, data)
+ function listener.onincoming(conn, data) -- luacheck: ignore 212/conn
if data then
resolver:feed(handler, data);
end
@@ -46,9 +45,12 @@ local function new_async_socket(sock, resolver)
resolver:servfail(conn); -- Let the magic commence
end
end
- handler, err = server.wrapclient(sock, "dns", 53, listener);
- if not handler then
- return nil, err;
+ do
+ local err;
+ handler, err = server.wrapclient(sock, "dns", 53, listener);
+ if not handler then
+ return nil, err;
+ end
end
handler.settimeout = function () end
@@ -89,7 +91,7 @@ function async_resolver_methods:lookup(handler, qname, qtype, qclass)
end)(resolver:peek(qname, qtype, qclass));
end
-function query_methods:cancel(call_handler, reason)
+function query_methods:cancel(call_handler, reason) -- luacheck: ignore 212/reason
log("warn", "Cancelling DNS lookup for %s", tostring(self[4]));
self[1].cancel(self[2], self[3], self[4], self[5], call_handler);
end
diff --git a/net/connlisteners.lua b/net/connlisteners.lua
deleted file mode 100644
index 9b8f88c3..00000000
--- a/net/connlisteners.lua
+++ /dev/null
@@ -1,18 +0,0 @@
--- COMPAT w/pre-0.9
-local log = require "util.logger".init("net.connlisteners");
-local traceback = debug.traceback;
-
-local _ENV = nil;
--- luacheck: std none
-
-local function fail()
- log("error", "Attempt to use legacy connlisteners API. For more info see https://prosody.im/doc/developers/network");
- log("error", "Legacy connlisteners API usage, %s", traceback("", 2));
-end
-
-return {
- register = fail;
- get = fail;
- start = fail;
- -- epic fail
-};
diff --git a/net/resolvers/basic.lua b/net/resolvers/basic.lua
index 9a3c9952..56f9c77d 100644
--- a/net/resolvers/basic.lua
+++ b/net/resolvers/basic.lua
@@ -1,5 +1,6 @@
local adns = require "net.adns";
local inet_pton = require "util.net".pton;
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local methods = {};
local resolver_mt = { __index = methods };
diff --git a/net/resolvers/manual.lua b/net/resolvers/manual.lua
index c0d4e5d5..dbc40256 100644
--- a/net/resolvers/manual.lua
+++ b/net/resolvers/manual.lua
@@ -1,5 +1,6 @@
local methods = {};
local resolver_mt = { __index = methods };
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
-- Find the next target to connect to, and
-- pass it to cb()
diff --git a/net/resolvers/service.lua b/net/resolvers/service.lua
index b5a2d821..d1b8556c 100644
--- a/net/resolvers/service.lua
+++ b/net/resolvers/service.lua
@@ -1,5 +1,6 @@
local adns = require "net.adns";
local basic = require "net.resolvers.basic";
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local methods = {};
local resolver_mt = { __index = methods };
diff --git a/net/server_epoll.lua b/net/server_epoll.lua
index 5c65d227..fdf006f6 100644
--- a/net/server_epoll.lua
+++ b/net/server_epoll.lua
@@ -39,7 +39,10 @@ local default_config = { __index = {
read_timeout = 14 * 60;
-- How long to wait for a socket to become writable after queuing data to send
- send_timeout = 60;
+ send_timeout = 180;
+
+ -- How long to wait for a socket to become writable after creation
+ connect_timeout = 20;
-- Some number possibly influencing how many pending connections can be accepted
tcp_backlog = 128;
@@ -120,9 +123,13 @@ local function runtimers(next_delay, min_wait)
end
local new_timeout = f(now);
if new_timeout then
- -- Schedule for 'delay' from the time actually scheduled,
- -- not from now, in order to prevent timer drift.
- timer[1] = t + new_timeout;
+ -- Schedule for 'delay' from the time actually scheduled, not from now,
+ -- in order to prevent timer drift, unless it already drifted way out of sync.
+ if (t + new_timeout) > ( now - new_timeout ) then
+ timer[1] = t + new_timeout;
+ else
+ timer[1] = now + new_timeout;
+ end
resort_timers = true;
else
t_remove(timers, i);
@@ -176,6 +183,7 @@ function interface:on(what, ...)
local ok, err = pcall(listener, self, ...);
if not ok then
log("error", "Error calling on%s: %s", what, err);
+ return;
end
return err;
end
@@ -423,8 +431,10 @@ function interface:write(data)
else
self.writebuffer = { data };
end
- self:setwritetimeout();
- self:set(nil, true);
+ if not self._write_lock then
+ self:setwritetimeout();
+ self:set(nil, true);
+ end
return #data;
end
interface.send = interface.write;
@@ -571,12 +581,14 @@ function interface:onacceptable()
client:init();
if self.tls_direct then
client:starttls(self.tls_ctx);
+ else
+ client:onconnect();
end
end
-- Initialization
function interface:init()
- self:setwritetimeout();
+ self:setwritetimeout(cfg.connect_timeout);
return self:add(true, true);
end
@@ -604,11 +616,23 @@ function interface:pausefor(t)
end);
end
+function interface:pause_writes()
+ self._write_lock = true;
+ self:setwritetimeout(false);
+ self:set(nil, false);
+end
+
+function interface:resume_writes()
+ self._write_lock = nil;
+ if self.writebuffer[1] then
+ self:setwritetimeout();
+ self:set(nil, true);
+ end
+end
+
-- Connected!
function interface:onconnect()
- if self.conn and not self.peername and self.conn.getpeername then
- self.peername, self.peerport = self.conn:getpeername();
- end
+ self:updatenames();
self.onconnect = noop;
self:on("connect");
end
diff --git a/net/server_event.lua b/net/server_event.lua
index 11bd6a29..70757e03 100644
--- a/net/server_event.lua
+++ b/net/server_event.lua
@@ -253,6 +253,7 @@ end
--TODO: Deprecate
function interface_mt:lock_read(switch)
+ log("warn", ":lock_read is deprecated, use :pasue() and :resume()");
if switch then
return self:pause();
else
@@ -272,6 +273,19 @@ function interface_mt:resume()
end
end
+function interface_mt:pause_writes()
+ return self:_lock(self.nointerface, self.noreading, true);
+end
+
+function interface_mt:resume_writes()
+ self:_lock(self.nointerface, self.noreading, false);
+ if self.writecallback and not self.eventwrite then
+ self.eventwrite = addevent( base, self.conn, EV_WRITE, self.writecallback, cfg.WRITE_TIMEOUT ); -- register callback
+ return true;
+ end
+end
+
+
function interface_mt:counter(c)
if c then
self._connections = self._connections + c
diff --git a/net/server_select.lua b/net/server_select.lua
index 1a40a6d3..f616116e 100644
--- a/net/server_select.lua
+++ b/net/server_select.lua
@@ -424,9 +424,8 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
bufferlen = bufferlen + #data
if bufferlen > maxsendlen then
_closelist[ handler ] = "send buffer exceeded" -- cannot close the client at the moment, have to wait to the end of the cycle
- handler.write = idfalse -- don't write anymore
return false
- elseif socket and not _sendlist[ socket ] then
+ elseif not nosend and socket and not _sendlist[ socket ] then
_sendlistlen = addsocket(_sendlist, socket, _sendlistlen)
end
bufferqueuelen = bufferqueuelen + 1
@@ -456,49 +455,57 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport
maxreadlen = readlen or maxreadlen
return bufferlen, maxreadlen, maxsendlen
end
- --TODO: Deprecate
handler.lock_read = function (self, switch)
+ out_error( "server.lua, lock_read() is deprecated, use pause() and resume()" )
if switch == true then
- local tmp = _readlistlen
- _readlistlen = removesocket( _readlist, socket, _readlistlen )
- _readtimes[ handler ] = nil
- if _readlistlen ~= tmp then
- noread = true
- end
+ return self:pause()
elseif switch == false then
- if noread then
- noread = false
- _readlistlen = addsocket(_readlist, socket, _readlistlen)
- _readtimes[ handler ] = _currenttime
- end
+ return self:resume()
end
return noread
end
handler.pause = function (self)
- return self:lock_read(true);
+ local tmp = _readlistlen
+ _readlistlen = removesocket( _readlist, socket, _readlistlen )
+ _readtimes[ handler ] = nil
+ if _readlistlen ~= tmp then
+ noread = true
+ end
+ return noread;
end
handler.resume = function (self)
- return self:lock_read(false);
+ if noread then
+ noread = false
+ _readlistlen = addsocket(_readlist, socket, _readlistlen)
+ _readtimes[ handler ] = _currenttime
+ end
+ return noread;
end
handler.lock = function( self, switch )
- handler.lock_read (switch)
+ out_error( "server.lua, lock() is deprecated" )
+ handler.lock_read (self, switch)
if switch == true then
- handler.write = idfalse
- local tmp = _sendlistlen
- _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
- _writetimes[ handler ] = nil
- if _sendlistlen ~= tmp then
- nosend = true
- end
+ handler.pause_writes (self)
elseif switch == false then
- handler.write = write
- if nosend then
- nosend = false
- write( "" )
- end
+ handler.resume_writes (self)
end
return noread, nosend
end
+ handler.pause_writes = function (self)
+ local tmp = _sendlistlen
+ _sendlistlen = removesocket( _sendlist, socket, _sendlistlen )
+ _writetimes[ handler ] = nil
+ if _sendlistlen ~= tmp then
+ nosend = true
+ end
+ end
+ handler.resume_writes = function (self)
+ if nosend then
+ nosend = false
+ write( "" )
+ end
+ end
+
local _readbuffer = function( ) -- this function reads data
local buffer, err, part = receive( socket, pattern ) -- receive buffer with "pattern"
if not err or (err == "wantread" or err == "timeout") then -- received something
diff --git a/net/websocket/frames.lua b/net/websocket/frames.lua
index ba25d261..86752109 100644
--- a/net/websocket/frames.lua
+++ b/net/websocket/frames.lua
@@ -9,20 +9,21 @@
local softreq = require "util.dependencies".softreq;
local random_bytes = require "util.random".bytes;
-local bit = assert(softreq"bit" or softreq"bit32",
+local bit = assert(softreq"bit32" or softreq"bit",
"No bit module found. See https://prosody.im/doc/depends#bitop");
local band = bit.band;
local bor = bit.bor;
local bxor = bit.bxor;
local lshift = bit.lshift;
local rshift = bit.rshift;
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local t_concat = table.concat;
local s_byte = string.byte;
local s_char= string.char;
local s_sub = string.sub;
-local s_pack = string.pack; -- luacheck: ignore 143
-local s_unpack = string.unpack; -- luacheck: ignore 143
+local s_pack = string.pack;
+local s_unpack = string.unpack;
if not s_pack and softreq"struct" then
s_pack = softreq"struct".pack;
diff --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua
index 1cbe27a4..7fae8983 100644
--- a/plugins/mod_admin_telnet.lua
+++ b/plugins/mod_admin_telnet.lua
@@ -22,6 +22,7 @@ local prosody = _G.prosody;
local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" };
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local iterators = require "util.iterators";
local keys, values = iterators.keys, iterators.values;
local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join");
@@ -30,6 +31,8 @@ local cert_verify_identity = require "util.x509".verify_identity;
local envload = require "util.envload".envload;
local envloadfile = require "util.envload".envloadfile;
local has_pposix, pposix = pcall(require, "util.pposix");
+local async = require "util.async";
+local serialize = require "util.serialization".new({ fatal = false, unquoted = true});
local commands = module:shared("commands")
local def_env = module:shared("env");
@@ -47,6 +50,21 @@ end
console = {};
+local runner_callbacks = {};
+
+function runner_callbacks:ready()
+ self.data.conn:resume();
+end
+
+function runner_callbacks:waiting()
+ self.data.conn:pause();
+end
+
+function runner_callbacks:error(err)
+ module:log("error", "Traceback[telnet]: %s", err);
+end
+
+
function console:new_session(conn)
local w = function(s) conn:write(s:gsub("\n", "\r\n")); end;
local session = { conn = conn;
@@ -62,6 +80,11 @@ function console:new_session(conn)
};
session.env = setmetatable({}, default_env_mt);
+ session.thread = async.runner(function (line)
+ console:process_line(session, line);
+ session.send(string.char(0));
+ end, runner_callbacks, session);
+
-- Load up environment with helper objects
for name, t in pairs(def_env) do
if type(t) == "table" then
@@ -150,8 +173,7 @@ function console_listener.onincoming(conn, data)
for line in data:gmatch("[^\n]*[\n\004]") do
if session.closed then return end
- console:process_line(session, line);
- session.send(string.char(0));
+ session.thread:run(line);
end
session.partial_data = data:match("[^\n]+$");
end
@@ -474,9 +496,12 @@ function def_env.config:load(filename, format)
return true, "Config loaded";
end
-function def_env.config:get(host, section, key)
+function def_env.config:get(host, key)
+ if key == nil then
+ host, key = "*", host;
+ end
local config_get = require "core.configmanager".get
- return true, tostring(config_get(host, section, key));
+ return true, serialize(config_get(host, key));
end
function def_env.config:reload()
@@ -520,6 +545,9 @@ local function session_flags(session, line)
if session.remote then
line[#line+1] = "(remote)";
end
+ if session.is_bidi then
+ line[#line+1] = "(bidi)";
+ end
return table.concat(line, " ");
end
@@ -1062,13 +1090,33 @@ end
def_env.xmpp = {};
local st = require "util.stanza";
-function def_env.xmpp:ping(localhost, remotehost)
- if prosody.hosts[localhost] then
- module:send(st.iq{ from=localhost, to=remotehost, type="get", id="ping" }
- :tag("ping", {xmlns="urn:xmpp:ping"}), prosody.hosts[localhost]);
- return true, "Sent ping";
+local new_id = require "util.id".medium;
+function def_env.xmpp:ping(localhost, remotehost, timeout)
+ localhost = select(2, jid_split(localhost));
+ remotehost = select(2, jid_split(remotehost));
+ if not localhost then
+ return nil, "Invalid sender hostname";
+ elseif not prosody.hosts[localhost] then
+ return nil, "No such local host";
+ end
+ if not remotehost then
+ return nil, "Invalid destination hostname";
+ elseif prosody.hosts[remotehost] then
+ return nil, "Both hosts are local";
+ end
+ local iq = st.iq{ from=localhost, to=remotehost, type="get", id=new_id()}
+ :tag("ping", {xmlns="urn:xmpp:ping"});
+ local ret, err;
+ local wait, done = async.waiter();
+ module:context(localhost):send_iq(iq, nil, timeout)
+ :next(function (ret_) ret = ret_; end,
+ function (err_) err = err_; end)
+ :finally(done);
+ wait();
+ if ret then
+ return true, "pong from " .. ret.stanza.attr.from;
else
- return nil, "No such host";
+ return false, tostring(err);
end
end
@@ -1207,7 +1255,7 @@ local function format_stat(type, value, ref_value)
--do return tostring(value) end
if type == "duration" then
if ref_value < 0.001 then
- return ("%d µs"):format(value*1000000);
+ return ("%g µs"):format(value*1000000);
elseif ref_value < 0.9 then
return ("%0.2f ms"):format(value*1000);
end
@@ -1495,7 +1543,7 @@ function def_env.stats:show(filter)
local stats, changed, extra = require "core.statsmanager".get_stats();
local available, displayed = 0, 0;
local displayed_stats = new_stats_context(self);
- for name, value in pairs(stats) do
+ for name, value in iterators.sorted_pairs(stats) do
available = available + 1;
if not filter or name:match(filter) then
displayed = displayed + 1;
diff --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua
index d4701148..082ed961 100644
--- a/plugins/mod_bosh.lua
+++ b/plugins/mod_bosh.lua
@@ -44,10 +44,11 @@ local bosh_max_polling = module:get_option_number("bosh_max_polling", 5);
local bosh_max_wait = module:get_option_number("bosh_max_wait", 120);
local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure");
-local cross_domain = module:get_option("cross_domain_bosh", false);
+local cross_domain = module:get_option("cross_domain_bosh");
-if cross_domain == true then cross_domain = "*"; end
-if type(cross_domain) == "table" then cross_domain = table.concat(cross_domain, ", "); end
+if cross_domain ~= nil then
+ module:log("info", "The 'cross_domain_bosh' option has been deprecated");
+end
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
@@ -91,22 +92,6 @@ function check_inactive(now, session, context, reason) -- luacheck: ignore 212/n
end
end
-local function set_cross_domain_headers(response)
- local headers = response.headers;
- headers.access_control_allow_methods = "GET, POST, OPTIONS";
- headers.access_control_allow_headers = "Content-Type";
- headers.access_control_max_age = "7200";
- headers.access_control_allow_origin = cross_domain;
- return response;
-end
-
-function handle_OPTIONS(event)
- if cross_domain and event.request.headers.origin then
- set_cross_domain_headers(event.response);
- end
- return "";
-end
-
function handle_POST(event)
log("debug", "Handling new request %s: %s\n----------", tostring(event.request), tostring(event.request.body));
@@ -121,10 +106,6 @@ function handle_POST(event)
local headers = response.headers;
headers.content_type = "text/xml; charset=utf-8";
- if cross_domain and request.headers.origin then
- set_cross_domain_headers(response);
- end
-
-- stream:feed() calls the stream_callbacks, so all stanzas in
-- the body are processed in this next line before it returns.
-- In particular, the streamopened() stream callback is where
@@ -511,8 +492,6 @@ module:provides("http", {
route = {
["GET"] = GET_response;
["GET /"] = GET_response;
- ["OPTIONS"] = handle_OPTIONS;
- ["OPTIONS /"] = handle_OPTIONS;
["POST"] = handle_POST;
["POST /"] = handle_POST;
};
diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua
index 8e31a968..8d7b92fe 100644
--- a/plugins/mod_c2s.lua
+++ b/plugins/mod_c2s.lua
@@ -106,7 +106,13 @@ function stream_callbacks.streamopened(session, attr)
if features.tags[1] or session.full_jid then
send(features);
else
- (session.log or log)("warn", "No stream features to offer");
+ if session.secure then
+ -- Normally STARTTLS would be offered
+ (session.log or log)("warn", "No stream features to offer on secure session. Check authentication settings.");
+ else
+ -- Here SASL should be offered
+ (session.log or log)("warn", "No stream features to offer on insecure session. Check encryption and security settings.");
+ end
session:close{ condition = "undefined-condition", text = "No stream features to proceed with" };
end
end
@@ -284,7 +290,7 @@ function listener.onconnect(conn)
if data then
local ok, err = stream:feed(data);
if not ok then
- log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+ log("debug", "Received invalid XML (%s) %d bytes: %q", tostring(err), #data, data:sub(1, 300));
session:close("not-well-formed");
end
end
diff --git a/plugins/mod_component.lua b/plugins/mod_component.lua
index b41204a2..b8c87dee 100644
--- a/plugins/mod_component.lua
+++ b/plugins/mod_component.lua
@@ -310,7 +310,7 @@ function listener.onconnect(conn)
function session.data(_, data)
local ok, err = stream:feed(data);
if ok then return; end
- module:log("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " "):gsub("[%z\1-\31]", "_"));
+ log("debug", "Received invalid XML (%s) %d bytes: %q", tostring(err), #data, data:sub(1, 300));
session:close("not-well-formed");
end
diff --git a/plugins/mod_http.lua b/plugins/mod_http.lua
index a1d409bd..829c2d02 100644
--- a/plugins/mod_http.lua
+++ b/plugins/mod_http.lua
@@ -14,6 +14,7 @@ local moduleapi = require "core.moduleapi";
local url_parse = require "socket.url".parse;
local url_build = require "socket.url".build;
local normalize_path = require "util.http".normalize_path;
+local set = require "util.set";
local server = require "net.http.server";
@@ -22,6 +23,11 @@ server.set_default_host(module:get_option_string("http_default_host"));
server.set_option("body_size_limit", module:get_option_number("http_max_content_size"));
server.set_option("buffer_size_limit", module:get_option_number("http_max_buffer_size"));
+-- CORS settigs
+local opt_methods = module:get_option_set("access_control_allow_methods", { "GET", "OPTIONS" });
+local opt_headers = module:get_option_set("access_control_allow_headers", { "Content-Type" });
+local opt_max_age = module:get_option_number("access_control_max_age", 2 * 60 * 60);
+
local function get_http_event(host, app_path, key)
local method, path = key:match("^(%S+)%s+(.+)$");
if not method then -- No path specified, default to "" (base path)
@@ -83,6 +89,13 @@ function moduleapi.http_url(module, app_name, default_path)
return "http://disabled.invalid/";
end
+local function apply_cors_headers(response, methods, headers, max_age, origin)
+ response.headers.access_control_allow_methods = tostring(methods);
+ response.headers.access_control_allow_headers = tostring(headers);
+ response.headers.access_control_max_age = tostring(max_age)
+ response.headers.access_control_allow_origin = origin or "*";
+end
+
function module.add_host(module)
local host = module.host;
if host ~= "*" then
@@ -101,9 +114,27 @@ function module.add_host(module)
end
apps[app_name] = apps[app_name] or {};
local app_handlers = apps[app_name];
+
+ local app_methods = opt_methods;
+
+ local function cors_handler(event_data)
+ local request, response = event_data.request, event_data.response;
+ apply_cors_headers(response, app_methods, opt_headers, opt_max_age, request.headers.origin);
+ end
+
+ local function options_handler(event_data)
+ cors_handler(event_data);
+ return "";
+ end
+
for key, handler in pairs(event.item.route or {}) do
local event_name = get_http_event(host, app_path, key);
if event_name then
+ local method = event_name:match("^%S+");
+ if not app_methods:contains(method) then
+ app_methods = app_methods + set.new{ method };
+ end
+ local options_event_name = event_name:gsub("^%S+", "OPTIONS");
if type(handler) ~= "function" then
local data = handler;
handler = function () return data; end
@@ -121,6 +152,8 @@ function module.add_host(module)
if not app_handlers[event_name] then
app_handlers[event_name] = handler;
module:hook_object_event(server, event_name, handler);
+ module:hook_object_event(server, event_name, cors_handler, 1);
+ module:hook_object_event(server, options_event_name, options_handler, -1);
else
module:log("warn", "App %s added handler twice for '%s', ignoring", app_name, event_name);
end
diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua
index 13473219..2bb13298 100644
--- a/plugins/mod_http_errors.lua
+++ b/plugins/mod_http_errors.lua
@@ -26,21 +26,24 @@ local html = [[
<meta charset="utf-8">
<title>{title}</title>
<style>
-body{
- margin-top:14%;
- text-align:center;
- background-color:#F8F8F8;
- font-family:sans-serif;
+body {
+ margin-top : 14%;
+ text-align : center;
+ background-color : #F8F8F8;
+ font-family : sans-serif
}
-h1{
- font-size:xx-large;
+
+h1 {
+ font-size : xx-large
}
-p{
- font-size:x-large;
+
+p {
+ font-size : x-large
}
+
p+p {
- font-size:large;
- font-family:courier;
+ font-size : large;
+ font-family : courier
}
</style>
</head>
diff --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua
index 94bedbb1..67bf177e 100644
--- a/plugins/mod_mam/mod_mam.lua
+++ b/plugins/mod_mam/mod_mam.lua
@@ -33,7 +33,7 @@ local is_stanza = st.is_stanza;
local tostring = tostring;
local time_now = os.time;
local m_min = math.min;
-local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
+local timestamp, timestamp_parse, datestamp = import( "util.datetime", "datetime", "parse", "date");
local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" });
@@ -46,13 +46,8 @@ if not archive.find then
end
local use_total = module:get_option_boolean("mam_include_total", true);
-local cleanup;
-
-local function schedule_cleanup(username)
- if cleanup and not cleanup[username] then
- table.insert(cleanup, username);
- cleanup[username] = true;
- end
+function schedule_cleanup()
+ -- replaced by non-noop later if cleanup is enabled
end
-- Handle prefs.
@@ -96,7 +91,6 @@ module:hook("iq-set/self/"..xmlns_mam..":query", function(event)
local qid = query.attr.queryid;
get_prefs(origin.username, true);
- schedule_cleanup(origin.username);
-- Search query parameters
local qwith, qstart, qend;
@@ -212,6 +206,7 @@ end
local function shall_store(user, who)
-- TODO Cache this?
if not um.user_exists(user, host) then
+ module:log("debug", "%s@%s does not exist", user, host)
return false;
end
local prefs = get_prefs(user);
@@ -329,6 +324,9 @@ module:hook("pre-message/full", strip_stanza_id_after_other_events, -1);
local cleanup_after = module:get_option_string("archive_expires_after", "1w");
local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
if cleanup_after ~= "never" then
+ local cleanup_storage = module:open_store("archive_cleanup");
+ local cleanup_map = module:open_store("archive_cleanup", "map");
+
local day = 86400;
local multipliers = { d = day, w = day * 7, m = 31 * day, y = 365.2425 * day };
local n, m = cleanup_after:lower():match("(%d+)%s*([dwmy]?)");
@@ -346,33 +344,50 @@ if cleanup_after ~= "never" then
return false;
end
- -- Set of known users to do message expiry for
- -- Populated either below or when new messages are added
- cleanup = {};
+ -- For each day, store a set of users that have new messages. To expire
+ -- messages, we collect the union of sets of users from dates that fall
+ -- outside the cleanup range.
- -- Iterating over users is not supported by all authentication modules
- -- Catch and ignore error if not supported
- pcall(function ()
- -- If this works, then we schedule cleanup for all known users on startup
- for user in um.users(module.host) do
- schedule_cleanup(user);
+ function schedule_cleanup(username, date)
+ cleanup_map:set(date or datestamp(), username, true);
+ end
+ local cleanup_time = module:measure("cleanup", "times");
+
+ cleanup_runner = require "util.async".runner(function ()
+ local cleanup_done = cleanup_time();
+ local users = {};
+ local cut_off = datestamp(os.time() - cleanup_after);
+ for date in cleanup_storage:users() do
+ if date <= cut_off then
+ module:log("debug", "Messages from %q should be expired", date);
+ local messages_this_day = cleanup_storage:get(date);
+ if messages_this_day then
+ for user in pairs(messages_this_day) do
+ users[user] = true;
+ end
+ if date < cut_off then
+ -- Messages from the same day as the cut-off might not have expired yet,
+ -- but all earlier will have, so clear storage for those days.
+ cleanup_storage:set(date, nil);
+ end
+ end
+ end
end
- end);
-
- -- At odd intervals, delete old messages for one user
- module:add_timer(math.random(10, 60), function()
- local user = table.remove(cleanup, 1);
- if user then
- module:log("debug", "Removing old messages for user %q", user);
+ local sum, num_users = 0, 0;
+ for user in pairs(users) do
local ok, err = archive:delete(user, { ["end"] = os.time() - cleanup_after; })
- if not ok then
- module:log("warn", "Could not expire archives for user %s: %s", user, err);
- elseif type(ok) == "number" then
- module:log("debug", "Removed %d messages", ok);
+ if ok then
+ num_users = num_users + 1;
+ sum = sum + (tonumber(ok) or 0);
end
- cleanup[user] = nil;
end
- return math.random(cleanup_interval, cleanup_interval * 2);
+ module:log("info", "Deleted %d expired messages for %d users", sum, num_users);
+ cleanup_done();
+ end);
+
+ cleanup_task = module:add_timer(1, function ()
+ cleanup_runner:run(true);
+ return cleanup_interval;
end);
else
module:log("debug", "Archive expiry disabled");
diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua
index 7a4aac2b..92a09bb0 100644
--- a/plugins/mod_pep.lua
+++ b/plugins/mod_pep.lua
@@ -8,6 +8,7 @@ local calculate_hash = require "util.caps".calculate_hash;
local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
local cache = require "util.cache";
local set = require "util.set";
+local new_id = require "util.id".medium;
local storagemanager = require "core.storagemanager";
local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
@@ -138,9 +139,7 @@ local function get_broadcaster(username)
if kind == "retract" then
kind = "items"; -- XEP-0060 signals retraction in an <items> container
end
- local message = st.message({ from = user_bare, type = "headline" })
- :tag("event", { xmlns = xmlns_pubsub_event })
- :tag(kind, { node = node });
+
if item then
item = st.clone(item);
item.attr.xmlns = nil; -- Clear the pubsub namespace
@@ -149,8 +148,17 @@ local function get_broadcaster(username)
item:maptags(function () return nil; end);
end
end
+ end
+
+ local id = new_id();
+ local message = st.message({ from = user_bare, type = "headline", id = id })
+ :tag("event", { xmlns = xmlns_pubsub_event })
+ :tag(kind, { node = node });
+
+ if item then
message:add_child(item);
end
+
for jid in pairs(jids) do
module:log("debug", "Sending notification to %s from %s: %s", jid, user_bare, tostring(item));
message.attr.to = jid;
@@ -252,9 +260,6 @@ end
module:hook("iq/bare/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
module:hook("iq/bare/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
-module:add_identity("pubsub", "pep", module:get_option_string("name", "Prosody"));
-module:add_feature("http://jabber.org/protocol/pubsub#publish");
-
local function get_caps_hash_from_presence(stanza, current)
local t = stanza.attr.type;
if not t then
diff --git a/plugins/mod_pep_simple.lua b/plugins/mod_pep_simple.lua
index f0b5d7ef..f91e5448 100644
--- a/plugins/mod_pep_simple.lua
+++ b/plugins/mod_pep_simple.lua
@@ -14,6 +14,7 @@ local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed
local pairs = pairs;
local next = next;
local type = type;
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local calculate_hash = require "util.caps".calculate_hash;
local core_post_stanza = prosody.core_post_stanza;
local bare_sessions = prosody.bare_sessions;
diff --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua
index 268a2f0c..f7f458ca 100644
--- a/plugins/mod_presence.lua
+++ b/plugins/mod_presence.lua
@@ -81,8 +81,14 @@ function handle_normal_presence(origin, stanza)
res.presence.attr.to = nil;
end
end
- for jid in pairs(roster[false].pending) do -- resend incoming subscription requests
- origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
+ for jid, pending_request in pairs(roster[false].pending) do -- resend incoming subscription requests
+ if type(pending_request) == "table" then
+ local subscribe = st.deserialize(pending_request);
+ subscribe.attr.type, subscribe.attr.from = "subscribe", jid;
+ origin.send(subscribe);
+ else
+ origin.send(st.presence({type="subscribe", from=jid}));
+ end
end
local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});
for jid, item in pairs(roster) do -- resend outgoing subscription requests
@@ -226,7 +232,7 @@ function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_b
else
core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
if not rostermanager.is_contact_pending_in(node, host, from_bare) then
- if rostermanager.set_contact_pending_in(node, host, from_bare) then
+ if rostermanager.set_contact_pending_in(node, host, from_bare, stanza) then
sessionmanager.send_to_available_resources(node, host, stanza);
end -- TODO else return error, unable to save
end
diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua
index 855c5fd2..05f80365 100644
--- a/plugins/mod_pubsub/mod_pubsub.lua
+++ b/plugins/mod_pubsub/mod_pubsub.lua
@@ -75,7 +75,7 @@ function simple_broadcast(kind, node, jids, item, actor, node_obj)
local msg_type = node_obj and node_obj.config.message_type or "headline";
local message = st.message({ from = module.host, type = msg_type, id = id })
:tag("event", { xmlns = xmlns_pubsub_event })
- :tag(kind, { node = node })
+ :tag(kind, { node = node });
if item then
message:add_child(item);
@@ -101,11 +101,12 @@ function simple_broadcast(kind, node, jids, item, actor, node_obj)
end
local max_max_items = module:get_option_number("pubsub_max_items", 256);
-function check_node_config(node, actor, new_config) -- luacheck: ignore 212/actor 212/node
+function check_node_config(node, actor, new_config) -- luacheck: ignore 212/node 212/actor
if (new_config["max_items"] or 1) > max_max_items then
return false;
end
- if new_config["access_model"] ~= "whitelist" and new_config["access_model"] ~= "open" then
+ if new_config["access_model"] ~= "whitelist"
+ and new_config["access_model"] ~= "open" then
return false;
end
return true;
diff --git a/plugins/mod_s2s/mod_s2s.lua b/plugins/mod_s2s/mod_s2s.lua
index aae37b7f..79308847 100644
--- a/plugins/mod_s2s/mod_s2s.lua
+++ b/plugins/mod_s2s/mod_s2s.lua
@@ -595,8 +595,7 @@ local function initialize_session(session)
if data then
local ok, err = stream:feed(data);
if ok then return; end
- log("warn", "Received invalid XML: %s", data);
- log("warn", "Problem was: %s", err);
+ log("debug", "Received invalid XML (%s) %d bytes: %q", tostring(err), #data, data:sub(1, 300));
session:close("not-well-formed");
end
end
diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua
index fba84ef8..ba30b9e6 100644
--- a/plugins/mod_saslauth.lua
+++ b/plugins/mod_saslauth.lua
@@ -275,7 +275,8 @@ module:hook("stream-features", function(event)
if mechanisms[1] then
features:add_child(mechanisms);
elseif not next(sasl_mechanisms) then
- log("warn", "No available SASL mechanisms, verify that the configured authentication module is working");
+ local authmod = module:get_option_string("authentication", "internal_plain");
+ log("error", "No available SASL mechanisms, verify that the configured authentication module '%s' is loaded and configured correctly", authmod);
else
log("warn", "All available authentication mechanisms are either disabled or not suitable for an insecure connection");
end
diff --git a/plugins/mod_storage_memory.lua b/plugins/mod_storage_memory.lua
index 1c6628e3..5a6ce6bf 100644
--- a/plugins/mod_storage_memory.lua
+++ b/plugins/mod_storage_memory.lua
@@ -22,6 +22,10 @@ local function _purge_store(self, username)
return true;
end
+local function _users(self)
+ return next, self.store, nil;
+end
+
local keyval_store = {};
keyval_store.__index = keyval_store;
@@ -39,9 +43,13 @@ end
keyval_store.purge = _purge_store;
+keyval_store.users = _users;
+
local archive_store = {};
archive_store.__index = archive_store;
+archive_store.users = _users;
+
function archive_store:append(username, key, value, when, with)
if is_stanza(value) then
value = st.preserialize(value);
diff --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua
index 56cef569..5c0c0208 100644
--- a/plugins/mod_storage_sql.lua
+++ b/plugins/mod_storage_sql.lua
@@ -11,7 +11,7 @@ local is_stanza = require"util.stanza".is_stanza;
local t_concat = table.concat;
local noop = function() end
-local unpack = table.unpack or unpack;
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local function iterator(result)
return function(result_)
local row = result_();
diff --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua
index 029ddd1d..4ead60dc 100644
--- a/plugins/mod_tls.lua
+++ b/plugins/mod_tls.lua
@@ -35,9 +35,10 @@ local host = hosts[module.host];
local ssl_ctx_c2s, ssl_ctx_s2sout, ssl_ctx_s2sin;
local ssl_cfg_c2s, ssl_cfg_s2sout, ssl_cfg_s2sin;
+local err_c2s, err_s2sin, err_s2sout;
function module.load()
- local NULL, err = {};
+ local NULL = {};
local modhost = module.host;
local parent = modhost:match("%.(.*)$");
@@ -52,14 +53,14 @@ function module.load()
local parent_s2s = rawgetopt(parent, "s2s_ssl") or NULL;
local host_s2s = rawgetopt(modhost, "s2s_ssl") or parent_s2s;
- ssl_ctx_c2s, err, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections
- if not ssl_ctx_c2s then module:log("error", "Error creating context for c2s: %s", err); end
+ ssl_ctx_c2s, err_c2s, ssl_cfg_c2s = create_context(host.host, "server", host_c2s, host_ssl, global_c2s); -- for incoming client connections
+ if not ssl_ctx_c2s then module:log("error", "Error creating context for c2s: %s", err_c2s); end
- ssl_ctx_s2sout, err, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s); -- for outgoing server connections
- if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err); end
+ ssl_ctx_s2sout, err_s2sout, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s); -- for outgoing server connections
+ if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err_s2sout); end
- ssl_ctx_s2sin, err, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s); -- for incoming server connections
- if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err); end
+ ssl_ctx_s2sin, err_s2sin, ssl_cfg_s2sin = create_context(host.host, "server", host_s2s, host_ssl, global_s2s); -- for incoming server connections
+ if not ssl_ctx_s2sin then module:log("error", "Error creating contexts for s2sin: %s", err_s2sin); end
end
module:hook_global("config-reloaded", module.load);
@@ -74,12 +75,21 @@ local function can_do_tls(session)
return session.ssl_ctx;
end
if session.type == "c2s_unauthed" then
+ if not ssl_ctx_c2s and c2s_require_encryption then
+ session.log("error", "No TLS context available for c2s. Earlier error was: %s", err_c2s);
+ end
session.ssl_ctx = ssl_ctx_c2s;
session.ssl_cfg = ssl_cfg_c2s;
elseif session.type == "s2sin_unauthed" and allow_s2s_tls then
+ if not ssl_ctx_s2sin and s2s_require_encryption then
+ session.log("error", "No TLS context available for s2sin. Earlier error was: %s", err_s2sin);
+ end
session.ssl_ctx = ssl_ctx_s2sin;
session.ssl_cfg = ssl_cfg_s2sin;
elseif session.direction == "outgoing" and allow_s2s_tls then
+ if not ssl_ctx_s2sout and s2s_require_encryption then
+ session.log("error", "No TLS context available for s2sout. Earlier error was: %s", err_s2sout);
+ end
session.ssl_ctx = ssl_ctx_s2sout;
session.ssl_cfg = ssl_cfg_s2sout;
else
diff --git a/plugins/mod_websocket.lua b/plugins/mod_websocket.lua
index b4aba338..008f6823 100644
--- a/plugins/mod_websocket.lua
+++ b/plugins/mod_websocket.lua
@@ -29,18 +29,10 @@ local t_concat = table.concat;
local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5);
local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure");
-local cross_domain = module:get_option_set("cross_domain_websocket", {});
-if cross_domain:contains("*") or cross_domain:contains(true) then
- cross_domain = true;
+local cross_domain = module:get_option("cross_domain_websocket");
+if cross_domain ~= nil then
+ module:log("info", "The 'cross_domain_websocket' option has been deprecated");
end
-
-local function check_origin(origin)
- if cross_domain == true then
- return true;
- end
- return cross_domain:contains(origin);
-end
-
local xmlns_framing = "urn:ietf:params:xml:ns:xmpp-framing";
local xmlns_streams = "http://etherx.jabber.org/streams";
local xmlns_client = "jabber:client";
@@ -158,11 +150,6 @@ function handle_request(event)
return 501;
end
- if not check_origin(request.headers.origin or "") then
- module:log("debug", "Origin %s is not allowed by 'cross_domain_websocket' [ %s ]", request.headers.origin or "(missing header)", cross_domain);
- return 403;
- end
-
local function websocket_close(code, message)
conn:write(build_close(code, message));
conn:close();
@@ -329,27 +316,4 @@ module:provides("http", {
function module.add_host(module)
module:hook("c2s-read-timeout", keepalive, -0.9);
-
- if cross_domain ~= true then
- local url = require "socket.url";
- local ws_url = module:http_url("websocket", "xmpp-websocket");
- local url_components = url.parse(ws_url);
- -- The 'Origin' consists of the base URL without path
- url_components.path = nil;
- local this_origin = url.build(url_components);
- local local_cross_domain = module:get_option_set("cross_domain_websocket", { this_origin });
- if local_cross_domain:contains(true) then
- module:log("error", "cross_domain_websocket = true only works in the global section");
- return;
- end
-
- -- Don't add / remove something added by another host
- -- This might be weird with random load order
- local_cross_domain:exclude(cross_domain);
- cross_domain:include(local_cross_domain);
- module:log("debug", "cross_domain = %s", tostring(cross_domain));
- function module.unload()
- cross_domain:exclude(local_cross_domain);
- end
- end
end
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index 954bae92..89e67744 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -453,7 +453,7 @@ for event_name, method in pairs {
if room == nil then
-- Watch presence to create rooms
- if stanza.attr.type == nil and stanza.name == "presence" then
+ if stanza.attr.type == nil and stanza.name == "presence" and stanza:get_child("x", "http://jabber.org/protocol/muc") then
room = muclib.new_room(room_jid);
return room:handle_first_presence(origin, stanza);
elseif stanza.attr.type ~= "error" then
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index 9648ea78..a8d3d790 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -23,6 +23,7 @@ local resourceprep = require "util.encodings".stringprep.resourceprep;
local st = require "util.stanza";
local base64 = require "util.encodings".base64;
local md5 = require "util.hashes".md5;
+local new_id = require "util.id".medium;
local log = module._log;
@@ -39,7 +40,7 @@ function room_mt:__tostring()
end
function room_mt.save()
- -- overriden by mod_muc.lua
+ -- overridden by mod_muc.lua
end
function room_mt:get_occupant_jid(real_jid)
@@ -279,7 +280,7 @@ function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason)
self_p = st.clone(base_presence):add_child(self_x);
end
- -- General populance
+ -- General populace
for occupant_nick, n_occupant in self:each_occupant() do
if occupant_nick ~= occupant.nick then
local pr;
@@ -428,13 +429,6 @@ module:hook("muc-occupant-pre-change", function(event)
end, 1);
function room_mt:handle_first_presence(origin, stanza)
- if not stanza:get_child("x", "http://jabber.org/protocol/muc") then
- module:log("debug", "Room creation without <x>, possibly desynced");
-
- origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
- return true;
- end
-
local real_jid = stanza.attr.from;
local dest_jid = stanza.attr.to;
local bare_jid = jid_bare(real_jid);
@@ -609,7 +603,7 @@ function room_mt:handle_normal_presence(origin, stanza)
x:tag("status", {code = "303";}):up();
x:tag("status", {code = "110";}):up();
self:route_stanza(generated_unavail:add_child(x));
- dest_nick = nil; -- set dest_nick to nil; so general populance doesn't see it for whole orig_occupant
+ dest_nick = nil; -- set dest_nick to nil; so general populace doesn't see it for whole orig_occupant
end
end
@@ -967,7 +961,7 @@ function room_mt:handle_admin_query_get_command(origin, stanza)
local _aff_rank = valid_affiliations[_aff or "none"];
local _rol = item.attr.role;
if _aff and _aff_rank and not _rol then
- -- You need to be at least an admin, and be requesting info about your affifiliation or lower
+ -- You need to be at least an admin, and be requesting info about your affiliation or lower
-- e.g. an admin can't ask for a list of owners
local affiliation_rank = valid_affiliations[affiliation or "none"];
if (affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank)
@@ -1044,6 +1038,9 @@ end
function room_mt:handle_groupchat_to_room(origin, stanza)
local from = stanza.attr.from;
local occupant = self:get_occupant_by_real_jid(from);
+ if not stanza.attr.id then
+ stanza.attr.id = new_id()
+ end
if module:fire_event("muc-occupant-groupchat", {
room = self; origin = origin; stanza = stanza; from = from; occupant = occupant;
}) then return true; end
@@ -1292,7 +1289,7 @@ function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
-- Outcast can be by host.
is_host_only and affiliation == "outcast" and select(2, jid_split(occupant.bare_jid)) == host
) then
- -- need to publcize in all cases; as affiliation in <item/> has changed.
+ -- need to publicize in all cases; as affiliation in <item/> has changed.
occupants_updated[occupant] = occupant.role;
if occupant.role ~= role and (
is_downgrade or
@@ -1371,6 +1368,42 @@ function room_mt:get_role(nick)
return occupant and occupant.role or nil;
end
+function room_mt:may_set_role(actor, occupant, role)
+ local event = {
+ room = self,
+ actor = actor,
+ occupant = occupant,
+ role = role,
+ };
+
+ module:fire_event("muc-pre-set-role", event);
+ if event.allowed ~= nil then
+ return event.allowed, event.error, event.condition;
+ end
+
+ -- Can't do anything to other owners or admins
+ local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
+ if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
+ return nil, "cancel", "not-allowed";
+ end
+
+ -- If you are trying to give or take moderator role you need to be an owner or admin
+ if occupant.role == "moderator" or role == "moderator" then
+ local actor_affiliation = self:get_affiliation(actor);
+ if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
+ return nil, "cancel", "not-allowed";
+ end
+ end
+
+ -- Need to be in the room and a moderator
+ local actor_occupant = self:get_occupant_by_real_jid(actor);
+ if not actor_occupant or actor_occupant.role ~= "moderator" then
+ return nil, "cancel", "not-allowed";
+ end
+
+ return true;
+end
+
function room_mt:set_role(actor, occupant_jid, role, reason)
if not actor then return nil, "modify", "not-acceptable"; end
@@ -1385,24 +1418,9 @@ function room_mt:set_role(actor, occupant_jid, role, reason)
if actor == true then
actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
else
- -- Can't do anything to other owners or admins
- local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
- if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
- return nil, "cancel", "not-allowed";
- end
-
- -- If you are trying to give or take moderator role you need to be an owner or admin
- if occupant.role == "moderator" or role == "moderator" then
- local actor_affiliation = self:get_affiliation(actor);
- if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
- return nil, "cancel", "not-allowed";
- end
- end
-
- -- Need to be in the room and a moderator
- local actor_occupant = self:get_occupant_by_real_jid(actor);
- if not actor_occupant or actor_occupant.role ~= "moderator" then
- return nil, "cancel", "not-allowed";
+ local allowed, err, condition = self:may_set_role(actor, occupant, role)
+ if not allowed then
+ return allowed, err, condition;
end
end
diff --git a/plugins/muc/subject.lib.lua b/plugins/muc/subject.lib.lua
index 938abf61..c8b99cc7 100644
--- a/plugins/muc/subject.lib.lua
+++ b/plugins/muc/subject.lib.lua
@@ -94,6 +94,12 @@ module:hook("muc-occupant-groupchat", function(event)
local stanza = event.stanza;
local subject = stanza:get_child("subject");
if subject then
+ if stanza:get_child("body") or stanza:get_child("thread") then
+ -- Note: A message with a <subject/> and a <body/> or a <subject/> and
+ -- a <thread/> is a legitimate message, but it SHALL NOT be interpreted
+ -- as a subject change.
+ return;
+ end
local room = event.room;
local occupant = event.occupant;
-- Role check for subject changes
diff --git a/prosodyctl b/prosodyctl
index 76de45a2..f6812e23 100755
--- a/prosodyctl
+++ b/prosodyctl
@@ -83,7 +83,7 @@ local jid_split = require "util.jid".prepped_split;
local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2;
-----------------------
local commands = {};
-local command = arg[1];
+local command = table.remove(arg, 1);
function commands.adduser(arg)
if not arg[1] or arg[1] == "--help" then
@@ -222,7 +222,7 @@ function commands.start(arg)
end
--luacheck: ignore 411/ret
- local ok, ret = prosodyctl.start(prosody.paths.source);
+ local ok, ret = prosodyctl.start(prosody.paths.source, arg[-1]);
if ok then
local daemonize = configmanager.get("*", "daemonize");
if daemonize == nil then
@@ -363,6 +363,13 @@ function commands.about(arg)
.."\n ";
end)));
print("");
+ local have_pposix, pposix = pcall(require, "util.pposix");
+ if have_pposix and pposix.uname then
+ print("# Operating system");
+ local uname, err = pposix.uname();
+ print(uname and uname.sysname .. " " .. uname.release or "Unknown POSIX", err or "");
+ print("");
+ end
print("# Lua environment");
print("Lua version: ", _G._VERSION);
print("");
@@ -806,7 +813,7 @@ function commands.check(arg)
print("Checking config...");
local deprecated = set.new({
"bosh_ports", "disallow_s2s", "no_daemonize", "anonymous_login", "require_encryption",
- "vcard_compatibility",
+ "vcard_compatibility", "cross_domain_bosh", "cross_domain_websocket"
});
local known_global_options = set.new({
"pidfile", "log", "plugin_paths", "prosody_user", "prosody_group", "daemonize",
@@ -1300,8 +1307,6 @@ local command_runner = async.runner(function ()
end
end
- table.remove(arg, 1);
-
local module = modulemanager.get_module("*", module_name);
if not module then
show_message("Failed to load module '"..module_name.."': Unknown error");
@@ -1365,7 +1370,7 @@ local command_runner = async.runner(function ()
os.exit(0);
end
- os.exit(commands[command]({ select(2, unpack(arg)) }));
+ os.exit(commands[command](arg));
end, watchers);
command_runner:run(true);
diff --git a/spec/core_storagemanager_spec.lua b/spec/core_storagemanager_spec.lua
index a0a8b5ef..fd2f8742 100644
--- a/spec/core_storagemanager_spec.lua
+++ b/spec/core_storagemanager_spec.lua
@@ -1,4 +1,4 @@
-local unpack = table.unpack or unpack;
+local unpack = table.unpack or unpack; -- luacheck: ignore 113
local server = require "net.server_select";
package.loaded["net.server"] = server;
diff --git a/spec/scansion/keep_full_sub_req.scs b/spec/scansion/keep_full_sub_req.scs
new file mode 100644
index 00000000..244c1d55
--- /dev/null
+++ b/spec/scansion/keep_full_sub_req.scs
@@ -0,0 +1,58 @@
+# server MUST keep a record of the complete presence stanza comprising the subscription request (#689)
+
+[Client] Alice
+ jid: pars-a@localhost
+ password: password
+
+[Client] Bob
+ jid: pars-b@localhost
+ password: password
+
+[Client] Bob's phone
+ jid: pars-b@localhost/phone
+ password: password
+
+---------
+
+Alice connects
+
+Alice sends:
+ <presence to="${Bob's JID}" type="subscribe">
+ <preauth xmlns="urn:xmpp:pars:0" token="1tMFqYDdKhfe2pwp" />
+ </presence>
+
+Alice disconnects
+
+Bob connects
+
+Bob sends:
+ <presence/>
+
+Bob receives:
+ <presence from="${Bob's full JID}"/>
+
+Bob receives:
+ <presence from="${Alice's JID}" type="subscribe">
+ <preauth xmlns="urn:xmpp:pars:0" token="1tMFqYDdKhfe2pwp" />
+ </presence>
+
+Bob disconnects
+
+# Works if they reconnect too
+
+Bob's phone connects
+
+Bob's phone sends:
+ <presence/>
+
+Bob's phone receives:
+ <presence from="${Bob's phone's full JID}"/>
+
+
+Bob's phone receives:
+ <presence from="${Alice's JID}" type="subscribe">
+ <preauth xmlns="urn:xmpp:pars:0" token="1tMFqYDdKhfe2pwp" />
+ </presence>
+
+Bob's phone disconnects
+
diff --git a/spec/scansion/muc_subject_issue_667.scs b/spec/scansion/muc_subject_issue_667.scs
new file mode 100644
index 00000000..74980073
--- /dev/null
+++ b/spec/scansion/muc_subject_issue_667.scs
@@ -0,0 +1,129 @@
+# #667 MUC message with subject and body SHALL NOT be interpreted as a subject change
+
+[Client] Romeo
+ password: password
+ jid: romeo@localhost
+
+-----
+
+Romeo connects
+
+# and creates a room
+Romeo sends:
+ <presence to="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+Romeo receives:
+ <presence from="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <status code="201"/>
+ <item affiliation="owner" role="moderator" jid="${Romeo's full JID}"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+# the default (empty) subject
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost">
+ <subject/>
+ </message>
+
+# this should be treated as a normal message
+Romeo sends:
+ <message to="issue667@conference.localhost" type="groupchat">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+# Resync
+Romeo sends:
+ <presence to="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+# Presences
+Romeo receives:
+ <presence from="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item affiliation="owner" role="moderator" jid="${Romeo's full JID}"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+# the still empty subject
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost">
+ <subject/>
+ </message>
+
+# this is a subject change
+Romeo sends:
+ <message to="issue667@conference.localhost" type="groupchat">
+ <subject>Something to talk about</subject>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Something to talk about</subject>
+ </message>
+
+# a message without <subject>
+Romeo sends:
+ <message to="issue667@conference.localhost" type="groupchat">
+ <body>Lorem ipsum dolor sit amet</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <body>Lorem ipsum dolor sit amet</body>
+ </message>
+
+# Resync
+Romeo sends:
+ <presence to="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc"/>
+ </presence>
+
+# Presences
+Romeo receives:
+ <presence from="issue667@conference.localhost/Romeo">
+ <x xmlns="http://jabber.org/protocol/muc#user">
+ <item affiliation="owner" role="moderator" jid="${Romeo's full JID}"/>
+ <status code="110"/>
+ </x>
+ </presence>
+
+# History
+# These have delay tags but we ignore those for now
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Greetings</subject>
+ <body>Hello everyone</body>
+ </message>
+
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <body>Lorem ipsum dolor sit amet</body>
+ </message>
+
+# Finally, the topic
+Romeo receives:
+ <message type="groupchat" from="issue667@conference.localhost/Romeo">
+ <subject>Something to talk about</subject>
+ </message>
+
+Romeo disconnects
+
diff --git a/spec/scansion/prosody.cfg.lua b/spec/scansion/prosody.cfg.lua
index f95ea31b..fd742db6 100644
--- a/spec/scansion/prosody.cfg.lua
+++ b/spec/scansion/prosody.cfg.lua
@@ -14,10 +14,11 @@ modules_enabled = {
-- Not essential, but recommended
"carbons"; -- Keep multiple clients in sync
- "pep"; -- Enables users to publish their mood, activity, playing music and more
+ "pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
"private"; -- Private XML storage (for room bookmarks, etc.)
"blocklist"; -- Allow users to block communications with other users
- "vcard"; -- Allow users to set vCards
+ "vcard4"; -- User profiles (stored in PEP)
+ "vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
-- Nice to have
"version"; -- Replies to server version requests
@@ -26,6 +27,11 @@ modules_enabled = {
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
"mam"; -- Store messages in an archive and allow users to access it
+ --"csi_simple"; -- Simple Mobile optimizations
+
+ -- Admin interfaces
+ --"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
+ --"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- HTTP modules
--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
diff --git a/spec/util_format_spec.lua b/spec/util_format_spec.lua
index 7e6a0c6e..b9652d19 100644
--- a/spec/util_format_spec.lua
+++ b/spec/util_format_spec.lua
@@ -5,10 +5,13 @@ describe("util.format", function()
it("should work", function()
assert.equal("hello", format("%s", "hello"));
assert.equal("<nil>", format("%s"));
+ assert.equal("<nil>", format("%d"));
+ assert.equal("<nil>", format("%q"));
assert.equal(" [<nil>]", format("", nil));
assert.equal("true", format("%s", true));
assert.equal("[true]", format("%d", true));
assert.equal("% [true]", format("%%", true));
+ assert.equal("{ }", format("%q", { }));
end);
end);
end);
diff --git a/spec/util_http_spec.lua b/spec/util_http_spec.lua
index 0f51a86c..d38645f7 100644
--- a/spec/util_http_spec.lua
+++ b/spec/util_http_spec.lua
@@ -28,6 +28,11 @@ describe("util.http", function()
it("should decode important URL characters", function()
assert.are.equal("This & that = something", http.urldecode("This%20%26%20that%20%3d%20something"), "Important URL chars escaped");
end);
+
+ it("should decode both lower and uppercase", function ()
+ assert.are.equal("This & that = {something}.", http.urldecode("This%20%26%20that%20%3D%20%7Bsomething%7D%2E"), "Important URL chars escaped");
+ end);
+
end);
describe("#formencode()", function()
diff --git a/spec/util_interpolation_spec.lua b/spec/util_interpolation_spec.lua
new file mode 100644
index 00000000..88d9f844
--- /dev/null
+++ b/spec/util_interpolation_spec.lua
@@ -0,0 +1,17 @@
+local template = [[
+{greet!}, {name?world}!
+]];
+local expect1 = [[
+Hello, WORLD!
+]];
+local expect2 = [[
+Hello, world!
+]];
+
+describe("util.interpolation", function ()
+ it("renders", function ()
+ local render = require "util.interpolation".new("%b{}", string.upper);
+ assert.equal(expect1, render(template, { greet = "Hello", name = "world" }));
+ assert.equal(expect2, render(template, { greet = "Hello" }));
+ end);
+end);
diff --git a/spec/util_stanza_spec.lua b/spec/util_stanza_spec.lua
index 6fbae41a..18e39554 100644
--- a/spec/util_stanza_spec.lua
+++ b/spec/util_stanza_spec.lua
@@ -95,20 +95,31 @@ describe("util.stanza", function()
describe("#iq()", function()
it("should create an iq stanza", function()
- local i = st.iq({ id = "foo" });
+ local i = st.iq({ type = "get", id = "foo" });
assert.are.equal("iq", i.name);
assert.are.equal("foo", i.attr.id);
+ assert.are.equal("get", i.attr.type);
end);
- it("should reject stanzas with no id", function ()
+ it("should reject stanzas with no attributes", function ()
assert.has.error_match(function ()
st.iq();
- end, "id attribute");
+ end, "attributes");
+ end);
+
+ it("should reject stanzas with no id", function ()
assert.has.error_match(function ()
- st.iq({ foo = "bar" });
+ st.iq({ type = "get" });
end, "id attribute");
end);
+
+ it("should reject stanzas with no type", function ()
+ assert.has.error_match(function ()
+ st.iq({ id = "foo" });
+ end, "type attribute");
+
+ end);
end);
describe("#presence()", function ()
diff --git a/spec/util_table_spec.lua b/spec/util_table_spec.lua
new file mode 100644
index 00000000..76f54b69
--- /dev/null
+++ b/spec/util_table_spec.lua
@@ -0,0 +1,17 @@
+local u_table = require "util.table";
+describe("util.table", function ()
+ describe("create()", function ()
+ it("works", function ()
+ -- Can't test the allocated sizes of the table, so what you gonna do?
+ assert.is.table(u_table.create(1,1));
+ end);
+ end);
+
+ describe("pack()", function ()
+ it("works", function ()
+ assert.same({ "lorem", "ipsum", "dolor", "sit", "amet", n = 5 }, u_table.pack("lorem", "ipsum", "dolor", "sit", "amet"));
+ end);
+ end);
+end);
+
+
diff --git a/util-src/pposix.c b/util-src/pposix.c
index 5c926603..169343b8 100644
--- a/util-src/pposix.c
+++ b/util-src/pposix.c
@@ -25,14 +25,18 @@
#define _DEFAULT_SOURCE
#endif
#endif
+
#if defined(__APPLE__)
#ifndef _DARWIN_C_SOURCE
#define _DARWIN_C_SOURCE
#endif
#endif
+
+#if ! defined(__FreeBSD__)
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
+#endif
#include <stdlib.h>
#include <math.h>
diff --git a/util-src/time.c b/util-src/time.c
index bfad52ee..bc6b5b1c 100644
--- a/util-src/time.c
+++ b/util-src/time.c
@@ -1,5 +1,5 @@
#ifndef _POSIX_C_SOURCE
-#define _POSIX_C_SOURCE 199309L
+#define _POSIX_C_SOURCE 200809L
#endif
#include <time.h>
diff --git a/util/error.lua b/util/error.lua
new file mode 100644
index 00000000..344dd274
--- /dev/null
+++ b/util/error.lua
@@ -0,0 +1,52 @@
+local error_mt = { __name = "error" };
+
+function error_mt:__tostring()
+ return ("error<%s:%s:%s>"):format(self.type, self.condition, self.text);
+end
+
+local function is_err(e)
+ return getmetatable(e) == error_mt;
+end
+
+local function new(e, context, registry)
+ local template = (registry and registry[e]) or e or {};
+ return setmetatable({
+ type = template.type or "cancel";
+ condition = template.condition or "undefined-condition";
+ text = template.text;
+
+ context = context or template.context or { _error_id = e };
+ }, error_mt);
+end
+
+local function coerce(ok, err, ...)
+ if ok or is_err(err) then
+ return ok, err, ...;
+ end
+
+ local new_err = setmetatable({
+ native = err;
+
+ type = "cancel";
+ condition = "undefined-condition";
+ }, error_mt);
+ return ok, new_err, ...;
+end
+
+local function from_stanza(stanza, context)
+ local error_type, condition, text = stanza:get_error();
+ return setmetatable({
+ type = error_type or "cancel";
+ condition = condition or "undefined-condition";
+ text = text;
+
+ context = context or { stanza = stanza };
+ }, error_mt);
+end
+
+return {
+ new = new;
+ coerce = coerce;
+ is_err = is_err;
+ from_stanza = from_stanza;
+}
diff --git a/util/format.lua b/util/format.lua
index c5e513fa..c31f599f 100644
--- a/util/format.lua
+++ b/util/format.lua
@@ -3,12 +3,14 @@
--
local tostring = tostring;
-local select = select;
local unpack = table.unpack or unpack; -- luacheck: ignore 113/unpack
+local pack = require "util.table".pack; -- TODO table.pack in 5.2+
local type = type;
+local dump = require "util.serialization".new("debug");
local function format(formatstring, ...)
- local args, args_length = { ... }, select('#', ...);
+ local args = pack(...);
+ local args_length = args.n;
-- format specifier spec:
-- 1. Start: '%%'
@@ -28,13 +30,15 @@ local function format(formatstring, ...)
if spec ~= "%%" then
i = i + 1;
local arg = args[i];
- if arg == nil then -- special handling for nil
- arg = "<nil>"
- args[i] = "<nil>";
- end
local option = spec:sub(-1);
- if option == "q" or option == "s" then -- arg should be string
+ if arg == nil then
+ args[i] = "nil";
+ spec = "<%s>";
+ elseif option == "q" then
+ args[i] = dump(arg);
+ spec = "%s";
+ elseif option == "s" then
args[i] = tostring(arg);
elseif type(arg) ~= "number" then -- arg isn't number as expected?
args[i] = tostring(arg);
diff --git a/util/http.lua b/util/http.lua
index cfb89193..3852f91c 100644
--- a/util/http.lua
+++ b/util/http.lua
@@ -6,24 +6,26 @@
--
local format, char = string.format, string.char;
-local pairs, ipairs, tonumber = pairs, ipairs, tonumber;
+local pairs, ipairs = pairs, ipairs;
local t_insert, t_concat = table.insert, table.concat;
+local url_codes = {};
+for i = 0, 255 do
+ local c = char(i);
+ local u = format("%%%02x", i);
+ url_codes[c] = u;
+ url_codes[u] = c;
+ url_codes[u:upper()] = c;
+end
local function urlencode(s)
- return s and (s:gsub("[^a-zA-Z0-9.~_-]", function (c) return format("%%%02x", c:byte()); end));
+ return s and (s:gsub("[^a-zA-Z0-9.~_-]", url_codes));
end
local function urldecode(s)
- return s and (s:gsub("%%(%x%x)", function (c) return char(tonumber(c,16)); end));
+ return s and (s:gsub("%%%x%x", url_codes));
end
local function _formencodepart(s)
- return s and (s:gsub("%W", function (c)
- if c ~= " " then
- return format("%%%02x", c:byte());
- else
- return "+";
- end
- end));
+ return s and (urlencode(s):gsub("%%20", "+"));
end
local function formencode(form)
diff --git a/util/import.lua b/util/import.lua
index 8ecfe43c..1007bc0a 100644
--- a/util/import.lua
+++ b/util/import.lua
@@ -8,7 +8,7 @@
-local unpack = table.unpack or unpack; --luacheck: ignore 113 143
+local unpack = table.unpack or unpack; --luacheck: ignore 113
local t_insert = table.insert;
function _G.import(module, ...)
local m = package.loaded[module] or require(module);
diff --git a/util/iterators.lua b/util/iterators.lua
index 302cca36..c03c2fd6 100644
--- a/util/iterators.lua
+++ b/util/iterators.lua
@@ -11,9 +11,9 @@
local it = {};
local t_insert = table.insert;
-local select, next = select, next;
-local unpack = table.unpack or unpack; --luacheck: ignore 113 143
-local pack = table.pack or function (...) return { n = select("#", ...), ... }; end -- luacheck: ignore 143
+local next = next;
+local unpack = table.unpack or unpack; --luacheck: ignore 113
+local pack = table.pack or require "util.table".pack;
local type = type;
local table, setmetatable = table, setmetatable;
diff --git a/util/multitable.lua b/util/multitable.lua
index 8d32ed8a..4f2cd972 100644
--- a/util/multitable.lua
+++ b/util/multitable.lua
@@ -9,7 +9,7 @@
local select = select;
local t_insert = table.insert;
local pairs, next, type = pairs, next, type;
-local unpack = table.unpack or unpack; --luacheck: ignore 113 143
+local unpack = table.unpack or unpack; --luacheck: ignore 113
local _ENV = nil;
-- luacheck: std none
diff --git a/util/promise.lua b/util/promise.lua
index 07c9c4dc..0b182b54 100644
--- a/util/promise.lua
+++ b/util/promise.lua
@@ -49,6 +49,9 @@ local function promise_settle(promise, new_state, new_next, cbs, value)
for _, cb in ipairs(cbs) do
cb(value);
end
+ -- No need to keep references to callbacks
+ promise._pending_on_fulfilled = nil;
+ promise._pending_on_rejected = nil;
return true;
end
diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua
index 5f0c4d12..9b627bde 100644
--- a/util/prosodyctl.lua
+++ b/util/prosodyctl.lua
@@ -229,7 +229,8 @@ local function isrunning()
return true, signal.kill(pid, 0) == 0;
end
-local function start(source_dir)
+local function start(source_dir, lua)
+ lua = lua and lua .. " " or "";
local ok, ret = isrunning();
if not ok then
return ok, ret;
@@ -238,9 +239,9 @@ local function start(source_dir)
return false, "already-running";
end
if not source_dir then
- os.execute("./prosody");
+ os.execute(lua .. "./prosody");
else
- os.execute(source_dir.."/../../bin/prosody");
+ os.execute(lua .. source_dir.."/../../bin/prosody");
end
return true;
end
diff --git a/util/serialization.lua b/util/serialization.lua
index dd6a2a2b..7ae77a3a 100644
--- a/util/serialization.lua
+++ b/util/serialization.lua
@@ -20,7 +20,6 @@ local pcall = pcall;
local envload = require"util.envload".envload;
local pos_inf, neg_inf = math.huge, -math.huge;
--- luacheck: ignore 143/math
local m_type = math.type or function (n)
return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float";
end;
diff --git a/util/stanza.lua b/util/stanza.lua
index a90d56b3..e9847ca6 100644
--- a/util/stanza.lua
+++ b/util/stanza.lua
@@ -423,9 +423,15 @@ local function message(attr, body)
end
end
local function iq(attr)
- if not (attr and attr.id) then
+ if not attr then
+ error("iq stanzas require id and type attributes");
+ end
+ if not attr.id then
error("iq stanzas require an id attribute");
end
+ if not attr.type then
+ error("iq stanzas require a type attribute");
+ end
return new_stanza("iq", attr);
end