diff options
55 files changed, 816 insertions, 220 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..dd199997 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,11 @@ clean: test: $(BUSTED) --lua=$(RUNWITH) +integration-test: all + $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua start + $(SCANSION) -d ./spec/scansion + $(RUNWITH) prosodyctl --config ./spec/scansion/prosody.cfg.lua stop + coverage: -rm -- luacov.* $(BUSTED) --lua=$(RUNWITH) -c @@ -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; @@ -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 ecb72a00..b2165b1d 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -120,9 +120,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 +180,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 +428,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,6 +578,8 @@ function interface:onacceptable() client:init(); if self.tls_direct then client:starttls(self.tls_ctx); + else + client:onconnect(); end end @@ -604,11 +613,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_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_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..d2ca709b 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 1d8c55bf..5e3f43f2 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 xmlns_pubsub = "http://jabber.org/protocol/pubsub"; local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; @@ -136,9 +137,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 @@ -147,8 +146,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; @@ -250,9 +258,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 40adcafe..0036b48f 100644 --- a/plugins/mod_pubsub/mod_pubsub.lua +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -73,7 +73,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); @@ -99,11 +99,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 ed04a5fb..3a9de1cc 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/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 ef2054b1..7259dde2 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 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 @@ -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 @@ -1365,7 +1365,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 |