Mercurial > prosody-hg
changeset 14138:c1469296190d
Merge 13.0->trunk
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Fri, 17 Apr 2026 14:35:32 +0100 |
| parents | 7008869fcce2 (current diff) 368df9b2d511 (diff) |
| children | 5aacae4b2ccf |
| files | net/server_epoll.lua plugins/mod_admin_shell.lua plugins/mod_bosh.lua plugins/mod_c2s.lua plugins/mod_s2s.lua util/startup.lua |
| diffstat | 12 files changed, 220 insertions(+), 62 deletions(-) [+] |
line wrap: on
line diff
--- a/core/configmanager.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/core/configmanager.lua Fri Apr 17 14:35:32 2026 +0100 @@ -90,7 +90,7 @@ config_format = config_format or filename:match("%w+$"); if config_format == "lua" then - local f, err = io.open(filename); + local f, err, errno = io.open(filename); if f then local new_config = setmetatable({ ["*"] = { } }, config_mt); local ok, err = parser.load(f:read("*a"), filename, new_config); @@ -100,7 +100,7 @@ end return ok, "parser", err; end - return f, "file", err; + return f, "file", err, errno; end if not config_format then
--- a/net/server_epoll.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/net/server_epoll.lua Fri Apr 17 14:35:32 2026 +0100 @@ -154,9 +154,8 @@ local closedtimers = {}; local function closetimer(id) - if timers:remove(id) then - closedtimers[id] = true; - end + closedtimers[id] = true; + timers:remove(id); end local function reschedule(id, time) @@ -697,6 +696,10 @@ -- make sure tls sockets aren't put in blocking mode if self.conn.shutdown and self._tls then self.conn:shutdown(); end self:del(); + if self._pausefor then + closetimer(self._pausefor); + self._pausefor = nil; + end self:setwritetimeout(false); self:setreadtimeout(false); self.onreadable = noop; @@ -961,6 +964,9 @@ self:set(false); self._pausefor = addtimer(t, function () self._pausefor = nil; + if not self.conn then + return; + end self:set(true); self:noise("Resuming after pause"); if self.conn and self.conn:dirty() then
--- a/plugins/mod_admin_shell.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/plugins/mod_admin_shell.lua Fri Apr 17 14:35:32 2026 +0100 @@ -421,6 +421,10 @@ end end + if (source or line):match("^%w+$") and def_env[source or line] then + source, line = nil, "help:"..(source or line).."();"; + end + local chunkname = "=console"; -- luacheck: ignore 311/err local chunk, err = envload(source or ("return "..line), chunkname, env);
--- a/plugins/mod_bosh.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/plugins/mod_bosh.lua Fri Apr 17 14:35:32 2026 +0100 @@ -45,7 +45,7 @@ local consider_bosh_secure = module:get_option_boolean("consider_bosh_secure"); local cross_domain = module:get_option("cross_domain_bosh"); -local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256, 10000); +local unauthed_stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 10000, 1000); if cross_domain ~= nil then module:log("info", "The 'cross_domain_bosh' option has been deprecated"); @@ -124,8 +124,9 @@ local body = request.body; local context = { request = request, response = response, notopen = true }; - local stream = new_xmpp_stream(context, stream_callbacks, stanza_size_limit); + local stream = new_xmpp_stream(context, stream_callbacks, unauthed_stanza_size_limit); response.context = context; + context.stream = stream; local headers = response.headers; headers.content_type = "text/xml; charset=utf-8"; @@ -336,6 +337,7 @@ session.bosh_responses = cache.new(BOSH_HOLD + 1):table(); session.requests = {}; session.send_buffer = {}; + session.stanza_size_limit = unauthed_stanza_size_limit; session.reset_stream = bosh_reset_stream; session.close = bosh_close_stream; session.dispatch_stanza = core_process_stanza; @@ -411,6 +413,11 @@ end session.conn = request.conn; + session.stream = context.stream; + + if session.stanza_size_limit then + session.stream:set_stanza_size_limit(session.stanza_size_limit); + end if session.rid then local diff = rid - session.rid; @@ -548,6 +555,7 @@ end function module.add_host(module) + module:depends("c2s"); -- updates stanza_size_limit on authentication module:depends("http"); module:provides("http", { default_path = "/http-bind";
--- a/plugins/mod_c2s.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/plugins/mod_c2s.lua Fri Apr 17 14:35:32 2026 +0100 @@ -8,7 +8,9 @@ module:set_global(); -local add_task = require "prosody.util.timer".add_task; +local timer = require "prosody.util.timer"; +local add_task = timer.add_task; +local stop_task = timer.stop; local new_xmpp_stream = require "prosody.util.xmppstream".new; local nameprep = require "prosody.util.encodings".stringprep.nameprep; local sessionmanager = require "prosody.core.sessionmanager"; @@ -25,10 +27,19 @@ local log = module._log; +local function stop_session_timers(session) + if session.c2s_timeout then + stop_task(session.c2s_timeout); + session.c2s_timeout = nil; + end +end + local c2s_timeout = module:get_option_period("c2s_timeout", "5 minutes"); local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5); local opt_keepalives = module:get_option_boolean("c2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); -local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000); +local unauthed_stanza_size_limit = module:get_option_integer("c2s_unauthed_stanza_size_limit", 10000,1000); +local authed_stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000); +local max_child_elements = module:get_option_integer("c2s_max_child_elements", 25000, 0); local advertised_idle_timeout = 14*60; -- default in all net.server implementations local network_settings = module:get_option("network_settings"); @@ -137,11 +148,11 @@ local features = st.stanza("stream:features"); hosts[session.host].events.fire_event("stream-features", { origin = session, features = features, stream = attr }); if features.tags[1] or session.full_jid then - if stanza_size_limit or advertised_idle_timeout then + if session.stanza_size_limit or advertised_idle_timeout then features:reset(); local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }); - if stanza_size_limit then - limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); + if session.stanza_size_limit then + limits:text_tag("max-bytes", string.format("%d", session.stanza_size_limit)); end if advertised_idle_timeout then limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); @@ -215,6 +226,7 @@ context:fire_event("pre-session-close", close_event_payload); reason = close_event_payload.reason; + stop_session_timers(session); if session.conn then if session.notopen then session:open_stream(); @@ -355,7 +367,11 @@ session.close = session_close; - local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); + session.stanza_size_limit = unauthed_stanza_size_limit; + + local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit, { + max_elements = max_child_elements; + }); session.stream = stream; session.notopen = true; @@ -409,6 +425,8 @@ session.dispatch_stanza = stream_callbacks.handlestanza; sessions[conn] = session; + + module:fire_event("c2s-connected", { conn = conn, session = session }); end function listener.onincoming(conn, data) @@ -422,6 +440,7 @@ local session = sessions[conn]; if session then (session.log or log)("info", "Client disconnected: %s", err or "connection closed"); + stop_session_timers(session); sm_destroy_session(session, err); session.conn = nil; sessions[conn] = nil; @@ -463,6 +482,13 @@ function module.add_host(module) module:hook("c2s-read-timeout", keepalive, -1); + module:hook("authentication-success", function(event) + local session = event.session; + if session.stream then + session.stanza_size_limit = authed_stanza_size_limit; + session.stream:set_stanza_size_limit(session.stanza_size_limit); + end + end); end module:hook("c2s-read-timeout", keepalive, -1);
--- a/plugins/mod_cloud_notify.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/plugins/mod_cloud_notify.lua Fri Apr 17 14:35:32 2026 +0100 @@ -74,8 +74,7 @@ -- Forward declarations, as both functions need to reference each other local handle_push_success, handle_push_error; -function handle_push_error(event) - local stanza = event.stanza; +function handle_push_error(stanza) local error_type, condition, error_text = stanza:get_error(); local node = id2node[stanza.attr.id]; local identifier = id2identifier[stanza.attr.id]; @@ -117,11 +116,6 @@ changed = true; user_push_services[push_identifier] = nil push_errors[push_identifier] = nil; - -- unhook iq handlers for this identifier (if possible) - module:unhook("iq-error/host/"..stanza.attr.id, handle_push_error); - module:unhook("iq-result/host/"..stanza.attr.id, handle_push_success); - id2node[stanza.attr.id] = nil; - id2identifier[stanza.attr.id] = nil; end elseif user_push_services[push_identifier] and user_push_services[push_identifier].jid == from and error_type == "wait" then module:log("debug", "Got error <%s:%s:%s> for identifier '%s': " @@ -141,8 +135,7 @@ return true; end -function handle_push_success(event) - local stanza = event.stanza; +function handle_push_success(stanza) local node = id2node[stanza.attr.id]; local identifier = id2identifier[stanza.attr.id]; if node == nil then return false; end -- unknown stanza? Ignore for now! @@ -153,11 +146,6 @@ if push_identifier == identifier then if user_push_services[push_identifier] and user_push_services[push_identifier].jid == from and push_errors[push_identifier] > 0 then push_errors[push_identifier] = 0; - -- unhook iq handlers for this identifier (if possible) - module:unhook("iq-error/host/"..stanza.attr.id, handle_push_error); - module:unhook("iq-result/host/"..stanza.attr.id, handle_push_success); - id2node[stanza.attr.id] = nil; - id2identifier[stanza.attr.id] = nil; module:log("debug", "Push succeeded, error count for identifier '%s' is now at %s again", push_identifier, tostring(push_errors[push_identifier]) ); @@ -257,14 +245,6 @@ end user_push_services[key] = nil; push_errors[key] = nil; - for stanza_id, identifier in pairs(id2identifier) do - if identifier == key then - module:unhook("iq-error/host/"..stanza_id, handle_push_error); - module:unhook("iq-result/host/"..stanza_id, handle_push_success); - id2node[stanza_id] = nil; - id2identifier[stanza_id] = nil; - end - end end end local ok = push_store:flush_to_disk(origin.username); @@ -413,11 +393,13 @@ if push_errors[push_identifier] == nil then push_errors[push_identifier] = 0; end - module:hook("iq-error/host/"..stanza_id, handle_push_error); - module:hook("iq-result/host/"..stanza_id, handle_push_success); id2node[stanza_id] = node; id2identifier[stanza_id] = push_identifier; - module:send(push_publish); + module:send_iq(push_publish):next(handle_push_success, handle_push_error):finally(function () + -- unhook iq handlers for this identifier (if possible) + id2node[stanza.attr.id] = nil; + id2identifier[stanza.attr.id] = nil; + end); pushes = pushes + 1; end end
--- a/plugins/mod_debug_reset.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/plugins/mod_debug_reset.lua Fri Apr 17 14:35:32 2026 +0100 @@ -16,7 +16,7 @@ module:fire_event("server-resetting"); for _, host in ipairs(hosts) do hostmanager.deactivate(host); - for i = 1, 4 do + for _ = 1, 4 do -- Run a few cycles to ensure full cleanup, including -- weak tables and finalizers collectgarbage("collect");
--- a/plugins/mod_limits.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/plugins/mod_limits.lua Fri Apr 17 14:35:32 2026 +0100 @@ -1,9 +1,13 @@ -- Because we deal with pre-authed sessions and streams we can't be host-specific module:set_global(); +local it = require "prosody.util.iterators"; local filters = require "prosody.util.filters"; local throttle = require "prosody.util.throttle"; local timer = require "prosody.util.timer"; + +local pton = require "prosody.util.net".pton; + local ceil = math.ceil; local limits_cfg = module:get_option("limits", {}); @@ -50,11 +54,51 @@ }; }; +local function get_truncated_ip(conn) + local session_ip = pton(conn:ip()); + if #session_ip == 128 then + -- Use /64 for IPv6 + session_ip = session_ip:sub(1, 8); + end + return session_ip; +end + +local function limit_ip_sessions(session_type, max_sessions) + -- [packed_truncated_ip] = count + local current_ips = module:shared("ips."..session_type); + module:hook(session_type.."-connected", function (event) + local session_ip = get_truncated_ip(event.conn); + local new_count = (current_ips[session_ip] or 0) + 1; + current_ips[session_ip] = new_count; + if new_count > max_sessions then + module:log("warn", "Rejecting stream from %s (%d of %d current connections)", event.conn:ip(), new_count, max_sessions); + event.session:close({ condition = "policy-violation", text = "too many connections" }); + end + end); + + module:hook(session_type.."-closed", function (event) + local session_ip = get_truncated_ip(event.conn); + local new_count = (current_ips[session_ip] or 0) - 1; + if new_count < 0 then + module:log("warn", "IP counting failure"); + new_count = 0; + end + if new_count == 0 then + current_ips[session_ip] = nil; + else + current_ips[session_ip] = new_count; + end + end); +end + for sess_type, sess_limits in pairs(limits_cfg) do limits[sess_type] = { bytes_per_second = parse_rate(sess_limits.rate, sess_type); burst_seconds = parse_burst(sess_limits.burst, sess_type); }; + if sess_limits.max_connections_per_ip then + limit_ip_sessions(sess_type, sess_limits.max_connections_per_ip); + end end if not limits.s2sin and limits.s2s then @@ -158,3 +202,42 @@ end end + +module:add_item("shell-command", { + section = "limits"; + section_desc = "Commands to inspect rate-limiting state"; + name = "connections"; + desc = "View per-IP connection limit status"; + args = { + { name = "ip", type = "string" }; + }; + handler = function(self, ip_str) -- luacheck: ignore 212/self + local check_ip = pton(ip_str); + if not check_ip then + return false, "Unable to parse IP address: "..ip_str; + end + if #check_ip == 128 then + check_ip = check_ip:sub(1, 8); + end + + local found, any_limits = 0; + + for sess_type, sess_limits in it.sorted_pairs(limits_cfg) do + if sess_limits.max_connections_per_ip then + any_limits = true; + local current_ips = module:shared("ips."..sess_type); + local count = current_ips[check_ip] or 0; + self.session.print(("%s: %d (limit %d)"):format(sess_type, count or 0, sess_limits.max_connections_per_ip)); + found = found + count; + end + end + + if not any_limits then + return false, "No per-IP limits are configured"; + end + + return true, ("Total of %d tracked connections with this IP"):format(found); + end; +}); + +
--- a/plugins/mod_s2s.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/plugins/mod_s2s.lua Fri Apr 17 14:35:32 2026 +0100 @@ -41,7 +41,9 @@ local secure_domains, insecure_domains = module:get_option_set("s2s_secure_domains", {})._items, module:get_option_set("s2s_insecure_domains", {})._items; local require_encryption = module:get_option_boolean("s2s_require_encryption", true); -local stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000); +local unauthed_stanza_size_limit = module:get_option_integer("s2s_unauthed_stanza_size_limit", 10000, 1000); +local authed_stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000); +local max_child_elements = module:get_option_integer("s2s_max_child_elements", 25000, 0); local sendq_size = module:get_option_integer("s2s_send_queue_size", 1024*32, 1); local block_retries = module:get_option_boolean("s2s_block_immediate_retries", false); @@ -247,6 +249,8 @@ end host_session.version = 1; + host_session.stanza_size_limit = unauthed_stanza_size_limit; + -- Store in buffer host_session.bounce_sendq = bounce_sendq; host_session.sendq = queue.new(sendq_size); @@ -291,10 +295,11 @@ module:hook("route/remote", route_to_existing_session, -1); module:hook("route/remote", route_to_new_session, -10); module:hook("s2sout-stream-features", function (event) - if not (stanza_size_limit or advertised_idle_timeout) then return end + local session = event.origin; + if not (session.stanza_size_limit or advertised_idle_timeout) then return end local limits = event.features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }) - if stanza_size_limit then - limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); + if session.stanza_size_limit then + limits:text_tag("max-bytes", string.format("%d", session.stanza_size_limit)); end if advertised_idle_timeout then limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); @@ -436,6 +441,9 @@ return false; end + session.stanza_size_limit = authed_stanza_size_limit; + session.stream:set_stanza_size_limit(session.stanza_size_limit); + if session.incoming and host then if not session.hosts[host] then session.hosts[host] = {}; end session.hosts[host].authed = true; @@ -594,11 +602,11 @@ end if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then - if stanza_size_limit or advertised_idle_timeout then + if session.stanza_size_limit or advertised_idle_timeout then features:reset(); local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }); - if stanza_size_limit then - limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); + if session.stanza_size_limit then + limits:text_tag("max-bytes", string.format("%d", session.stanza_size_limit)); end if advertised_idle_timeout then limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); @@ -815,7 +823,9 @@ -- Session initialization logic shared by incoming and outgoing local function initialize_session(session) - local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); + local stream = new_xmpp_stream(session, stream_callbacks, session.stanza_size_limit, { + max_elements = max_child_elements; + }); session.thread = runner(function (item) if st.is_stanza(item) then @@ -933,7 +943,7 @@ module:fire_event("s2sout-connected", { session = session }) session:open_stream(session.from_host, session.to_host); end - module:fire_event("s2s-connected", { session = session }) + module:fire_event("s2s-connected", { session = session, conn = conn }) session.ip = conn:ip(); end
--- a/util/prosodyctl.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/util/prosodyctl.lua Fri Apr 17 14:35:32 2026 +0100 @@ -14,7 +14,6 @@ local usermanager = require "prosody.core.usermanager"; local interpolation = require "prosody.util.interpolation"; local signal = require "prosody.util.signal"; -local set = require "prosody.util.set"; local path = require"prosody.util.paths"; local lfs = require "lfs"; local type = type; @@ -134,8 +133,7 @@ pidfile = config.resolve_relative_path(prosody.paths.data, pidfile); - local modules_disabled = set.new(config.get("*", "modules_disabled")); - if prosody.platform ~= "posix" or modules_disabled:contains("posix") then + if prosody.platform ~= "posix" then return false, "no-posix"; end
--- a/util/startup.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/util/startup.lua Fri Apr 17 14:35:32 2026 +0100 @@ -101,7 +101,7 @@ else config.set_credential_fallback_mode("warn"); end - local ok, level, err = config.load(filename); + local ok, level, err, errno = config.load(filename); if not ok then print("\n"); print("**************************"); @@ -117,10 +117,26 @@ end print(""); elseif level == "file" then - print("Prosody was unable to find the configuration file."); - print("We looked for: "..filename); - print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist"); - print("Copy or rename it to prosody.cfg.lua and edit as necessary."); + local err_kind; + if errno == 2 then + err_kind = "find"; + elseif errno == 13 then + err_kind = "access"; + else err_kind = "open"; + end + print(("Prosody was unable to %s the configuration file:"):format(err_kind)); + print(filename); + print(""); + if err_kind == "access" then + print(prosody.process_type.." does not have permission to open the file."); + print("Try running the command again as a user with permission to read the config file."); + elseif err_kind == "find" then + print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist"); + print("Copy or rename it to "..filename.." and edit as necessary."); + else + print(err); + end + print(""); end print("More help on configuring Prosody can be found at https://prosody.im/doc/configure"); print("Good luck!");
--- a/util/xmppstream.lua Thu Apr 02 20:54:46 2026 +0100 +++ b/util/xmppstream.lua Fri Apr 17 14:35:32 2026 +0100 @@ -22,7 +22,7 @@ local lxp_supports_xmldecl = pcall(lxp.new, { XmlDecl = false }); local lxp_supports_bytecount = not not lxp.new({}).getcurrentbytecount; -local default_stanza_size_limit = 1024*1024*1; -- 1MB +local default_stanza_size_limit = 10000; -- Minimum stanza size limit local _ENV = nil; -- luacheck: std none @@ -43,7 +43,7 @@ local function dummy_cb() end -local function new_sax_handlers(session, stream_callbacks, cb_handleprogress) +local function new_sax_handlers(session, stream_callbacks, cb_handleprogress, max_elements) local xml_handlers = {}; local cb_streamopened = stream_callbacks.streamopened; @@ -69,6 +69,7 @@ local stack = {}; local chardata, stanza = {}; local stanza_size = 0; + local child_quota = 0; local non_streamns_depth = 0; function xml_handlers:StartElement(tagname, attr) if stanza and #chardata > 0 then @@ -86,6 +87,18 @@ non_streamns_depth = non_streamns_depth + 1; end + if stanza and child_quota then + -- Check stanza complexity limits + child_quota = child_quota - 1; + if child_quota <= 0 then + cb_error(session, "parse-error", "resource-constraint", "Stanza exceeds permitted children"); + if not self.stop or not self:stop() then + error("Failed to abort parsing"); + end + return; + end + end + for i=1,#attr do local k = attr[i]; attr[i] = nil; @@ -122,6 +135,7 @@ end stanza = setmetatable({ name = name, attr = attr, tags = {} }, stanza_mt); + child_quota = max_elements; else -- we are inside a stanza, so add a tag if lxp_supports_bytecount then stanza_size = stanza_size + self:getcurrentbytecount(); @@ -237,10 +251,18 @@ session = new_session; end - return xml_handlers, { reset = reset, set_session = set_session }; + local function set_max_elements(new_max_elements) + max_elements = new_max_elements; + end + + return xml_handlers, { + reset = reset; + set_session = set_session; + set_max_elements = set_max_elements; + }; end -local function new(session, stream_callbacks, stanza_size_limit) +local function new(session, stream_callbacks, stanza_size_limit, ex) -- Used to track parser progress (e.g. to enforce size limits) local n_outstanding_bytes = 0; local handle_progress; @@ -253,8 +275,10 @@ error("Stanza size limits are not supported on this version of LuaExpat") end + local max_elements = ex and ex.max_elements; + local handlers, meta = new_sax_handlers(session, stream_callbacks, handle_progress); - local parser = new_parser(handlers, ns_separator, false); + local parser = new_parser(handlers, ns_separator, false, max_elements); local parse = parser.parse; function session.open_stream(session, from, to) -- luacheck: ignore 432/session @@ -298,8 +322,9 @@ return ok, err; end, set_session = meta.set_session; - set_stanza_size_limit = function (_, new_stanza_size_limit) + set_stanza_size_limit = function (_, new_stanza_size_limit, new_max_elements) stanza_size_limit = new_stanza_size_limit; + meta.set_max_elements(new_max_elements); end; }; end
