# HG changeset patch # User Matthew Wild # Date 1711553943 0 # Node ID 5d9ec2e55d74113d5e38ee6ad0262e75d08faf1d # Parent 2dbc169aae6a04f3e028b6d5c155f2e1bb8d2b69# Parent 54a936345aaafb5265cf1dc6b8b9463db46f4756 Merge 0.12->trunk diff -r 54a936345aaa -r 5d9ec2e55d74 .editorconfig --- a/.editorconfig Wed Mar 27 15:35:15 2024 +0000 +++ b/.editorconfig Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,14 @@ +# https://editorconfig.org/ + root = true [*] +charset = utf-8 end_of_line = lf indent_style = tab insert_final_newline = true trim_trailing_whitespace = true +max_line_length = 150 [CHANGES] indent_size = 4 @@ -14,6 +18,7 @@ indent_size = 3 indent_style = space -[**.xml] +[*.xml] +# xmllint --nsclean --encode UTF-8 --noent --format - indent_size = 2 indent_style = space diff -r 54a936345aaa -r 5d9ec2e55d74 .lua-format --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.lua-format Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,32 @@ +align_args: false +align_parameter: false +align_table_field: true +break_after_functioncall_lp: false +break_after_functiondef_lp: false +break_after_operator: false +break_after_table_lb: true +break_before_functioncall_rp: false +break_before_functiondef_rp: false +break_before_table_rb: true +chop_down_kv_table: true +chop_down_parameter: false +chop_down_table: true +column_limit: 150 +column_table_limit: 120 +continuation_indent_width: 1 +double_quote_to_single_quote: false +extra_sep_at_table_end: true +indent_width: 1 +keep_simple_control_block_one_line: true +keep_simple_function_one_line: true +line_breaks_after_function_body: 1 +line_separator: input +single_quote_to_double_quote: false +spaces_around_equals_in_field: true +spaces_before_call: 1 +spaces_inside_functioncall_parens: false +spaces_inside_functiondef_parens: false +spaces_inside_table_braces: true +tab_width: 1 +table_sep: ";" +use_tab: true diff -r 54a936345aaa -r 5d9ec2e55d74 .luacheckrc --- a/.luacheckrc Wed Mar 27 15:35:15 2024 +0000 +++ b/.luacheckrc Wed Mar 27 15:39:03 2024 +0000 @@ -2,7 +2,7 @@ codes = true ignore = { "411/err", "421/err", "411/ok", "421/ok", "211/_ENV", "431/log", "214", "581" } -std = "lua53c" +std = "lua54c" max_line_length = 150 read_globals = { @@ -61,7 +61,10 @@ "module.hourly", "module.broadcast", "module.context", + "module.could", "module.depends", + "module.default_permission", + "module.default_permissions", "module.fire_event", "module.get_directory", "module.get_host", @@ -71,9 +74,12 @@ "module.get_option", "module.get_option_array", "module.get_option_boolean", + "module.get_option_enum", "module.get_option_inherited_set", + "module.get_option_integer", "module.get_option_number", "module.get_option_path", + "module.get_option_period", "module.get_option_scalar", "module.get_option_set", "module.get_option_string", @@ -86,8 +92,10 @@ "module.load_resource", "module.log", "module.log_status", + "module.may", "module.measure", "module.metric", + "module.on_ready", "module.open_store", "module.provides", "module.remove_item", @@ -149,15 +157,10 @@ "net/dns.lua"; "net/server_select.lua"; - "util/vcard.lua"; - - "plugins/mod_storage_sql1.lua"; - "spec/core_moduleapi_spec.lua"; "spec/util_http_spec.lua"; "spec/util_ip_spec.lua"; "spec/util_multitable_spec.lua"; - "spec/util_rfc6724_spec.lua"; "spec/util_throttle_spec.lua"; "tools/ejabberd2prosody.lua"; @@ -171,6 +174,7 @@ "tools/migration/migrator/prosody_sql.lua"; "tools/migration/prosody-migrator.lua"; "tools/openfire2prosody.lua"; + "tools/test_mutants.sh.lua"; "tools/xep227toprosody.lua"; } for _, file in ipairs(exclude_files) do diff -r 54a936345aaa -r 5d9ec2e55d74 .semgrep.yml --- a/.semgrep.yml Wed Mar 27 15:35:15 2024 +0000 +++ b/.semgrep.yml Wed Mar 27 15:39:03 2024 +0000 @@ -22,3 +22,9 @@ message: Non-string default from :get_option_string severity: ERROR languages: [lua] +- id: stanza-empty-text-constructor + patterns: + - pattern: $A:text() + message: Use :get_text() to read text, or pass a value here to add text + severity: WARNING + languages: [lua] diff -r 54a936345aaa -r 5d9ec2e55d74 CHANGES --- a/CHANGES Wed Mar 27 15:35:15 2024 +0000 +++ b/CHANGES Wed Mar 27 15:39:03 2024 +0000 @@ -1,3 +1,78 @@ +TRUNK +===== + +## New + +### Administration + +- Add 'watch log' command to follow live debug logs at runtime (even if disabled) + +### Networking + +- Honour 'weight' parameter during SRV record selection +- Support for RFC 8305 "Happy Eyeballs" to improve IPv4/IPv6 connectivity +- Support for TCP Fast Open in server_epoll (pending LuaSocket support) +- Support for deferred accept in server_epoll (pending LuaSocket support) + +### MUC + +- Permissions updates: + - Room creation restricted to local users (of the parent host) by default + - restrict_room_creation = true restricts to admins, false disables all restrictions + - Persistent rooms can only be created by local users (parent host) by default + - muc_room_allow_persistent = false restricts to admins + - Public rooms can only be created by local users (parent host) by default + - muc_room_allow_public = false restricts to admins +- Commands to show occupants and affiliations in the Shell +- Save 'reason' text supplied with affiliation change + +### Security and authentication + +- Advertise supported SASL Channel-Binding types (XEP-0440) +- Implement RFC 9266 'tls-exporter' channel binding with TLS 1.3 +- Implement 'tls-server-end-point' channel binding +- New role and permissions framework and API +- Ability to disable and enable user accounts +- Full DANE support for s2s +- A "grace period" is now supported for deletion requests via in-band registration + +### Storage + +- New 'keyval+' combined keyval/map store type +- Performance improvements in internal archive stores +- Ability to use SQLite3 storage using LuaSQLite3 instead of LuaDBI + +### Module API + +- Config interface API can require that string values be picked from a provided set +- Acceptable interval can be specified for number options +- Method for parsing time periods / intervals from config +- Method for retrieving integer settings from config +- It is now easy for modules to expose a Prosody shell command, by adding a shell-command item +- Modules can now implement a module.ready method which will be called after server initialization + +### Configuration + +- The configuration file now supports referring and appending to options previously set +- Direct usage of the Lua API in the config file is deprecated, but can now be accessed via Lua.* instead + +## Changes + +- Support sub-second precision timestamps +- mod_blocklist: New option 'migrate_legacy_blocking' to disable migration from mod_privacy +- Moved all modules into the Lua namespace `prosody.` +- Forwarded header from RFC 7239 supported, disabled by default +- mod_http_file_share now uses roles framework, affecting access from e.g. components +- Intervals of mod_cron managed periodic jobs made configurable +- When mod_smacks is enabled, s2s connections not responding to ack requests are closed. +- Arguments to `prosodyctl shell` that start with ':' are now turned into method calls + +## Removed + +- Lua 5.1 support +- XEP-0090 support removed from mod_time +- util.rfc6724 + 0.12.0 ====== diff -r 54a936345aaa -r 5d9ec2e55d74 GNUmakefile --- a/GNUmakefile Wed Mar 27 15:35:15 2024 +0000 +++ b/GNUmakefile Wed Mar 27 15:39:03 2024 +0000 @@ -42,6 +42,10 @@ $(INSTALL_EXEC) ./prosody.install $(BIN)/prosody $(INSTALL_EXEC) ./prosodyctl.install $(BIN)/prosodyctl +install-loader: + $(MKDIR) $(SOURCE) + $(INSTALL_DATA) loader.lua $(SOURCE) + install-core: $(MKDIR) $(SOURCE) $(MKDIR) $(SOURCE)/core @@ -71,12 +75,13 @@ install-plugins: $(MKDIR) $(MODULES) - $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam + $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam $(MODULES)/mod_debug_stanzas $(INSTALL_DATA) plugins/*.lua $(MODULES) $(INSTALL_DATA) plugins/mod_pubsub/*.lua $(MODULES)/mod_pubsub $(INSTALL_DATA) plugins/adhoc/*.lua $(MODULES)/adhoc $(INSTALL_DATA) plugins/muc/*.lua $(MODULES)/muc $(INSTALL_DATA) plugins/mod_mam/*.lua $(MODULES)/mod_mam + $(INSTALL_DATA) plugins/mod_debug_stanzas/*.lua $(MODULES)/mod_debug_stanzas install-man: $(MKDIR) $(MAN)/man1 @@ -88,7 +93,7 @@ install-data: $(MKDIR_PRIVATE) $(DATA) -install: install-util install-net install-core install-plugins install-bin install-etc install-man install-meta install-data +install: install-util install-net install-core install-plugins install-bin install-etc install-man install-meta install-data install-loader clean: rm -f prosody.install @@ -98,10 +103,10 @@ $(MAKE) clean -C util-src test: - $(BUSTED) --lua=$(RUNWITH) + $(BUSTED) --helper loader --lua=$(RUNWITH) test-%: - $(BUSTED) --lua=$(RUNWITH) -r $* + $(BUSTED) --helper loader --lua=$(RUNWITH) -r $* integration-test: all $(MKDIR) data @@ -130,7 +135,7 @@ @echo $$(sed -n '/^\tlocal exclude_files/,/^}/p;' .luacheckrc | sed '1d;$d' | wc -l) files ignored shellcheck configure -vpath %.tl teal-src/ +vpath %.tl teal-src/prosody %.lua: %.tl tl -I teal-src/ --gen-compat off --gen-target 5.1 gen $^ -o $@ -lua-format -i $@ diff -r 54a936345aaa -r 5d9ec2e55d74 certs/openssl.cnf --- a/certs/openssl.cnf Wed Mar 27 15:35:15 2024 +0000 +++ b/certs/openssl.cnf Wed Mar 27 15:39:03 2024 +0000 @@ -46,7 +46,7 @@ [ subject_alternative_name ] -# See http://tools.ietf.org/html/rfc6120#section-13.7.1.2 for more info. +# See https://www.rfc-editor.org/rfc/rfc6120.html#section-13.7.1.2 for more info. DNS.0 = example.com otherName.0 = xmppAddr;FORMAT:UTF8,UTF8:example.com diff -r 54a936345aaa -r 5d9ec2e55d74 configure --- a/configure Wed Mar 27 15:35:15 2024 +0000 +++ b/configure Wed Mar 27 15:39:03 2024 +0000 @@ -45,7 +45,7 @@ Default is \$PREFIX/lib --datadir=DIR Location where the server data should be stored. Default is \$PREFIX/var/lib/$APP_DIRNAME ---lua-version=VERSION Use specific Lua version: 5.1, 5.2, or 5.3 +--lua-version=VERSION Use specific Lua version: 5.2, 5.3, or 5.4 Default is auto-detected. --lua-suffix=SUFFIX Versioning suffix to use in Lua filenames. Default is "$LUA_SUFFIX" (lua$LUA_SUFFIX...) @@ -173,7 +173,8 @@ --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" ] || [ "$LUA_VERSION" = "5.4" ] || die "Invalid Lua version in flag $key." + [ "$LUA_VERSION" != "5.1" ] || die "Lua 5.1 is no longer supported" + [ "$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) @@ -275,11 +276,11 @@ CFLAGS="$CFLAGS -ggdb" fi if [ "$OSPRESET" = "freebsd" ] || [ "$OSPRESET" = "openbsd" ]; then - LUA_INCDIR="/usr/local/include/lua51" + LUA_INCDIR="/usr/local/include/lua52" LUA_INCDIR_SET=yes CFLAGS="-Wall -fPIC -I/usr/local/include" LDFLAGS="-I/usr/local/include -L/usr/local/lib -shared" - LUA_SUFFIX="51" + LUA_SUFFIX="52" LUA_SUFFIX_SET=yes LUA_DIR=/usr/local LUA_DIR_SET=yes @@ -291,16 +292,16 @@ LUA_INCDIR_SET="yes" fi if [ "$OSPRESET" = "netbsd" ]; then - LUA_INCDIR="/usr/pkg/include/lua-5.1" + LUA_INCDIR="/usr/pkg/include/lua-5.2" LUA_INCDIR_SET=yes - LUA_LIBDIR="/usr/pkg/lib/lua/5.1" + LUA_LIBDIR="/usr/pkg/lib/lua/5.2" LUA_LIBDIR_SET=yes CFLAGS="-Wall -fPIC -I/usr/pkg/include" LDFLAGS="-L/usr/pkg/lib -Wl,-rpath,/usr/pkg/lib -shared" fi if [ "$OSPRESET" = "pkg-config" ]; then if [ "$LUA_SUFFIX_SET" != "yes" ]; then - LUA_SUFFIX="5.1"; + LUA_SUFFIX="5.4"; LUA_SUFFIX_SET=yes fi LUA_CF="$(pkg-config --cflags-only-I lua"$LUA_SUFFIX")" @@ -335,7 +336,7 @@ fi detect_lua_version() { - detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[1234])$"))' 2> /dev/null) + detected_lua=$("$1" -e 'print(_VERSION:match(" (5%.[234])$"))' 2> /dev/null) if [ "$detected_lua" != "nil" ] then if [ "$LUA_VERSION_SET" != "yes" ] @@ -389,10 +390,7 @@ lua_interp_found=no if [ "$LUA_SUFFIX_SET" != "yes" ] then - if [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.1" ] - then - suffixes="5.1 51 -5.1 -51" - elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.2" ] + if [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.2" ] then suffixes="5.2 52 -5.2 -52" elif [ "$LUA_VERSION_SET" = "yes" ] && [ "$LUA_VERSION" = "5.3" ] @@ -402,8 +400,7 @@ then suffixes="5.4 54 -5.4 -54" else - suffixes="5.1 51 -5.1 -51" - suffixes="$suffixes 5.2 52 -5.2 -52" + suffixes="5.2 52 -5.2 -52" suffixes="$suffixes 5.3 53 -5.3 -53" suffixes="$suffixes 5.4 54 -5.4 -54" fi diff -r 54a936345aaa -r 5d9ec2e55d74 core/certmanager.lua --- a/core/certmanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/certmanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,15 +6,13 @@ -- COPYING file in the source package for more information. -- -local ssl = require "ssl"; -local configmanager = require "core.configmanager"; -local log = require "util.logger".init("certmanager"); -local ssl_context = ssl.context or require "ssl.context"; -local ssl_newcontext = ssl.newcontext; -local new_config = require"util.sslconfig".new; +local configmanager = require "prosody.core.configmanager"; +local log = require "prosody.util.logger".init("certmanager"); +local new_config = require"prosody.net.server".tls_builder; +local tls = require "prosody.net.tls_luasec"; local stat = require "lfs".attributes; -local x509 = require "util.x509"; +local x509 = require "prosody.util.x509"; local lfs = require "lfs"; local tonumber, tostring = tonumber, tostring; @@ -28,33 +26,10 @@ local pcall = pcall; local prosody = prosody; -local pathutil = require"util.paths"; +local pathutil = require"prosody.util.paths"; local resolve_path = pathutil.resolve_relative_path; local config_path = prosody.paths.config or "."; -local function test_option(option) - return not not ssl_newcontext({mode="server",protocol="sslv23",options={ option }}); -end - -local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)"); -local luasec_version = tonumber(luasec_major) * 100 + tonumber(luasec_minor); -local luasec_has = ssl.config or { - algorithms = { - ec = luasec_version >= 5; - }; - capabilities = { - curves_list = luasec_version >= 7; - }; - options = { - cipher_server_preference = test_option("cipher_server_preference"); - no_ticket = test_option("no_ticket"); - no_compression = test_option("no_compression"); - single_dh_use = test_option("single_dh_use"); - single_ecdh_use = test_option("single_ecdh_use"); - no_renegotiation = test_option("no_renegotiation"); - }; -}; - local _ENV = nil; -- luacheck: std none @@ -122,7 +97,7 @@ local firstline = f:read(); if firstline == "-----BEGIN CERTIFICATE-----" and lfs.attributes(find_matching_key(full), "mode") == "file" then f:seek("set") - local cert = ssl.loadcertificate(f:read("*a")) + local cert = tls.load_certificate(f:read("*a")) -- TODO if more than one cert is found for a name, the most recently -- issued one should be used. -- for now, just filter out expired certs @@ -207,18 +182,18 @@ protocol = "tlsv1+"; verify = "none"; options = { - cipher_server_preference = luasec_has.options.cipher_server_preference; - no_ticket = luasec_has.options.no_ticket; - no_compression = luasec_has.options.no_compression and configmanager.get("*", "ssl_compression") ~= true; - single_dh_use = luasec_has.options.single_dh_use; - single_ecdh_use = luasec_has.options.single_ecdh_use; - no_renegotiation = luasec_has.options.no_renegotiation; + cipher_server_preference = tls.features.options.cipher_server_preference; + no_ticket = tls.features.options.no_ticket; + no_compression = tls.features.options.no_compression and configmanager.get("*", "ssl_compression") ~= true; + single_dh_use = tls.features.options.single_dh_use; + single_ecdh_use = tls.features.options.single_ecdh_use; + no_renegotiation = tls.features.options.no_renegotiation; }; verifyext = { "lsec_continue", -- Continue past certificate verification errors "lsec_ignore_purpose", -- Validate client certificates as if they were server certificates }; - curve = luasec_has.algorithms.ec and not luasec_has.capabilities.curves_list and "secp384r1"; + curve = tls.features.algorithms.ec and not tls.features.capabilities.curves_list and "secp384r1"; curveslist = { "X25519", "P-384", @@ -235,7 +210,7 @@ "!3DES", -- 3DES - slow and of questionable security "!aNULL", -- Ciphers that does not authenticate the connection }; - dane = luasec_has.capabilities.dane and configmanager.get("*", "use_dane") and { "no_ee_namechecks" }; + dane = tls.features.capabilities.dane and configmanager.get("*", "use_dane") and { "no_ee_namechecks" }; } local mozilla_ssl_configs = { @@ -304,9 +279,9 @@ }; -if luasec_has.curves then +if tls.features.curves then for i = #core_defaults.curveslist, 1, -1 do - if not luasec_has.curves[ core_defaults.curveslist[i] ] then + if not tls.features.curves[ core_defaults.curveslist[i] ] then t_remove(core_defaults.curveslist, i); end end @@ -314,10 +289,6 @@ core_defaults.curveslist = nil; end -local path_options = { -- These we pass through resolve_path() - key = true, certificate = true, cafile = true, capath = true, dhparam = true -} - local function create_context(host, mode, ...) local cfg = new_config(); cfg:apply(core_defaults); @@ -351,39 +322,12 @@ if mode == "server" then if not user_ssl_config.certificate then - log("info", "No certificate present in SSL/TLS configuration for %s. SNI will be required.", host); + log("debug", "No certificate present in SSL/TLS configuration for %s. SNI will be required.", host); end if user_ssl_config.certificate and not user_ssl_config.key then return nil, "No key present in SSL/TLS configuration for "..host; end end - for option in pairs(path_options) do - if type(user_ssl_config[option]) == "string" then - user_ssl_config[option] = resolve_path(config_path, user_ssl_config[option]); - else - user_ssl_config[option] = nil; - end - end - - -- LuaSec expects dhparam to be a callback that takes two arguments. - -- We ignore those because it is mostly used for having a separate - -- set of params for EXPORT ciphers, which we don't have by default. - if type(user_ssl_config.dhparam) == "string" then - local f, err = io_open(user_ssl_config.dhparam); - if not f then return nil, "Could not open DH parameters: "..err end - local dhparam = f:read("*a"); - f:close(); - user_ssl_config.dhparam = function() return dhparam; end - end - - local ctx, err = ssl_newcontext(user_ssl_config); - - -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care - -- of it ourselves (W/A for #x) - if ctx and user_ssl_config.ciphers then - local success; - success, err = ssl_context.setcipher(ctx, user_ssl_config.ciphers); - if not success then ctx = nil; end - end + local ctx, err = cfg:build(); if not ctx then err = err or "invalid ssl config" @@ -422,10 +366,16 @@ local function reload_ssl_config() global_ssl_config = configmanager.get("*", "ssl"); global_certificates = configmanager.get("*", "certificates") or "certs"; - if luasec_has.options.no_compression then + if tls.features.options.no_compression then core_defaults.options.no_compression = configmanager.get("*", "ssl_compression") ~= true; end - core_defaults.dane = configmanager.get("*", "use_dane") or false; + if not configmanager.get("*", "use_dane") then + core_defaults.dane = false; + elseif tls.features.capabilities.dane then + core_defaults.dane = { "no_ee_namechecks" }; + else + core_defaults.dane = true; + end cert_index = index_certs(resolve_path(config_path, global_certificates)); end diff -r 54a936345aaa -r 5d9ec2e55d74 core/configmanager.lua --- a/core/configmanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/configmanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -11,14 +11,15 @@ setmetatable, rawget, rawset, io, os, error, dofile, type, pairs, ipairs; local format, math_max, t_insert = string.format, math.max, table.insert; -local envload = require"util.envload".envload; -local deps = require"util.dependencies"; -local resolve_relative_path = require"util.paths".resolve_relative_path; -local glob_to_pattern = require"util.paths".glob_to_pattern; +local envload = require"prosody.util.envload".envload; +local deps = require"prosody.util.dependencies"; +local it = require"prosody.util.iterators"; +local resolve_relative_path = require"prosody.util.paths".resolve_relative_path; +local glob_to_pattern = require"prosody.util.paths".glob_to_pattern; local path_sep = package.config:sub(1,1); -local get_traceback_table = require "util.debug".get_traceback_table; +local get_traceback_table = require "prosody.util.debug".get_traceback_table; -local encodings = deps.softreq"util.encodings"; +local encodings = deps.softreq"prosody.util.encodings"; local nameprep = encodings and encodings.stringprep.nameprep or function (host) return host:lower(); end local _M = {}; @@ -40,16 +41,10 @@ return config; end -function _M.get(host, key, _oldkey) - if key == "core" then - key = _oldkey; -- COMPAT with code that still uses "core" - end +function _M.get(host, key) return config[host][key]; end -function _M.rawget(host, key, _oldkey) - if key == "core" then - key = _oldkey; -- COMPAT with code that still uses "core" - end +function _M.rawget(host, key) local hostconfig = rawget(config, host); if hostconfig then return rawget(hostconfig, key); @@ -68,10 +63,17 @@ return false; end -function _M.set(host, key, value, _oldvalue) - if key == "core" then - key, value = value, _oldvalue; --COMPAT with code that still uses "core" +local function rawget_option(config_table, host, key) + if host and key then + local hostconfig = rawget(config_table, host); + if not hostconfig then + return nil; + end + return rawget(hostconfig, key); end +end + +function _M.set(host, key, value) return set(config, host, key, value); end @@ -114,6 +116,51 @@ end end end + + local config_option_proxy_mt = { + __index = setmetatable({ + append = function (self, value) + local original_option = self:value(); + if original_option == nil then + original_option = {}; + end + if type(value) ~= "table" then + error("'append' operation expects a list of values to append to the existing list", 2); + end + if value[1] ~= nil then + for _, v in ipairs(value) do + t_insert(original_option, v); + end + else + for k, v in pairs(value) do + original_option[k] = v; + end + end + set(self.config_table, self.host, self.option_name, original_option); + return self; + end; + value = function (self) + return rawget_option(self.config_table, self.host, self.option_name); + end; + values = function (self) + return it.values(self:value()); + end; + }, { + __index = function (t, k) --luacheck: ignore 212/t + error("Unknown config option operation: '"..k.."'", 2); + end; + }); + + __call = function (self, v2) + local v = self:value() or {}; + if type(v) == "table" and type(v2) == "table" then + return self:append(v2); + end + + error("Invalid syntax - missing '=' perhaps?", 2); + end; + }; + parser = {}; function parser.load(data, config_file, config_table) local set_options = {}; -- set_options[host.."/"..option_name] = true (when the option has been set already in this file) @@ -128,7 +175,37 @@ if k:match("^ENV_") then return os.getenv(k:sub(5)); end - return rawget(_G, k); + if k == "Lua" then + return _G; + end + local val = rawget_option(config_table, env.__currenthost or "*", k); + + local g_val = rawget(_G, k); + + if val ~= nil or g_val == nil then + if type(val) == "table" then + return setmetatable({ + config_table = config_table; + host = env.__currenthost or "*"; + option_name = k; + }, config_option_proxy_mt); + end + return val; + end + + if g_val ~= nil then + t_insert( + warnings, + ("%s:%d: direct usage of the Lua API is deprecated - replace `%s` with `Lua.%s`"):format( + config_file, + get_line_number(config_file), + k, + k + ) + ); + end + + return g_val; end, __newindex = function (_, k, v) local host = env.__currenthost or "*"; diff -r 54a936345aaa -r 5d9ec2e55d74 core/features.lua --- a/core/features.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/features.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,33 @@ -local set = require "util.set"; +local set = require "prosody.util.set"; return { available = set.new{ -- mod_bookmarks bundled "mod_bookmarks"; + -- mod_server_info bundled + "mod_server_info"; + -- Roles, module.may and per-session authz + "permissions"; + -- prosody.* namespace + "loader"; + -- "keyval+" store + "keyval+"; "s2sout-pre-connect-event"; + + -- prosody:guest, prosody:registered, prosody:member + "split-user-roles"; + + -- new moduleapi methods + "getopt-enum"; + "getopt-interval"; + "getopt-period"; + "getopt-integer"; + + -- new module.ready() + "module-ready"; + + -- SIGUSR1 and 2 events + "signal-events"; }; }; diff -r 54a936345aaa -r 5d9ec2e55d74 core/hostmanager.lua --- a/core/hostmanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/hostmanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,18 +6,18 @@ -- COPYING file in the source package for more information. -- -local configmanager = require "core.configmanager"; -local modulemanager = require "core.modulemanager"; -local events_new = require "util.events".new; -local disco_items = require "util.multitable".new(); +local configmanager = require "prosody.core.configmanager"; +local modulemanager = require "prosody.core.modulemanager"; +local events_new = require "prosody.util.events".new; +local disco_items = require "prosody.util.multitable".new(); local NULL = {}; -local log = require "util.logger".init("hostmanager"); +local log = require "prosody.util.logger".init("hostmanager"); local hosts = prosody.hosts; local prosody_events = prosody.events; if not _G.prosody.incoming_s2s then - require "core.s2smanager"; + require "prosody.core.s2smanager"; end local incoming_s2s = _G.prosody.incoming_s2s; local core_route_stanza = _G.prosody.core_route_stanza; diff -r 54a936345aaa -r 5d9ec2e55d74 core/loggingmanager.lua --- a/core/loggingmanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/loggingmanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,20 +6,20 @@ -- COPYING file in the source package for more information. -- -local format = require "util.format".format; +local format = require "prosody.util.format".format; local setmetatable, rawset, pairs, ipairs, type = setmetatable, rawset, pairs, ipairs, type; local stdout = io.stdout; local io_open = io.open; local math_max, rep = math.max, string.rep; local os_date = os.date; -local getstyle, getstring = require "util.termcolours".getstyle, require "util.termcolours".getstring; -local st = require "util.stanza"; +local getstyle, getstring = require "prosody.util.termcolours".getstyle, require "prosody.util.termcolours".getstring; +local st = require "prosody.util.stanza"; -local config = require "core.configmanager"; -local logger = require "util.logger"; +local config = require "prosody.core.configmanager"; +local logger = require "prosody.util.logger"; -local have_pposix, pposix = pcall(require, "util.pposix"); +local have_pposix, pposix = pcall(require, "prosody.util.pposix"); have_pposix = have_pposix and pposix._VERSION == "0.4.0"; local _ENV = nil; diff -r 54a936345aaa -r 5d9ec2e55d74 core/moduleapi.lua --- a/core/moduleapi.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/moduleapi.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,28 +6,30 @@ -- COPYING file in the source package for more information. -- -local array = require "util.array"; -local set = require "util.set"; -local it = require "util.iterators"; -local logger = require "util.logger"; -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 errors = require "util.error"; -local promise = require "util.promise"; -local time_now = require "util.time".now; -local format = require "util.format".format; -local jid_node = require "util.jid".node; -local jid_resource = require "util.jid".resource; +local array = require "prosody.util.array"; +local set = require "prosody.util.set"; +local it = require "prosody.util.iterators"; +local logger = require "prosody.util.logger"; +local timer = require "prosody.util.timer"; +local resolve_relative_path = require"prosody.util.paths".resolve_relative_path; +local st = require "prosody.util.stanza"; +local cache = require "prosody.util.cache"; +local errors = require "prosody.util.error"; +local promise = require "prosody.util.promise"; +local time_now = require "prosody.util.time".now; +local format = require "prosody.util.format".format; +local jid_node = require "prosody.util.jid".node; +local jid_split = require "prosody.util.jid".split; +local jid_resource = require "prosody.util.jid".resource; +local human_io = require "prosody.util.human.io"; 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 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 pack = table.pack; +local unpack = table.unpack; local prosody = prosody; local hosts = prosody.hosts; @@ -128,14 +130,14 @@ end function api:require(lib) - local modulemanager = require"core.modulemanager"; + local modulemanager = require"prosody.core.modulemanager"; local f, n = modulemanager.loader:load_code_ext(self.name, lib, "lib.lua", self.environment); if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message return f(); end function api:depends(name) - local modulemanager = require"core.modulemanager"; + local modulemanager = require"prosody.core.modulemanager"; if self:get_option_inherited_set("modules_disabled", {}):contains(name) then error("Dependency on disabled module mod_"..name); end @@ -168,6 +170,10 @@ end end self.dependencies[name] = true; + if not mod.module.reverse_dependencies then + mod.module.reverse_dependencies = {}; + end + mod.module.reverse_dependencies[self.name] = true; return mod; end @@ -200,7 +206,7 @@ end function api:get_option(name, default_value) - local config = require "core.configmanager"; + local config = require "prosody.core.configmanager"; local value = config.get(self.host, name); if value == nil then value = default_value; @@ -227,12 +233,91 @@ return tostring(value); end -function api:get_option_number(name, ...) - local value = self:get_option_scalar(name, ...); +function api:get_option_number(name, default_value, min, max) + local value = self:get_option_scalar(name, default_value); local ret = tonumber(value); if value ~= nil and ret == nil then self:log("error", "Config option '%s' not understood, expecting a number", name); end + if ret == default_value then + -- skip interval checks for default or nil + return ret; + end + if min and ret < min then + self:log("warn", "Config option '%s' out of bounds %g < %g", name, ret, min); + return min; + end + if max and ret > max then + self:log("warn", "Config option '%s' out of bounds %g > %g", name, ret, max); + return max; + end + return ret; +end + +function api:get_option_integer(name, default_value, min, max) + local value = self:get_option_number(name, default_value, min or math.mininteger or -2 ^ 52, max or math.maxinteger or 2 ^ 53); + if value == default_value then + -- pass default trough unaltered, violates ranges sometimes + return value; + end + if math.type(value) == "float" then + self:log("warn", "Config option '%s' expected an integer, not a float (%g)", name, value) + return math.floor(value); + end + -- nil or an integer + return value; +end + +function api:get_option_period(name, default_value, min, max) + local value = self:get_option_scalar(name, default_value); + + local ret; + if value == "never" or value == false then + -- usually for disabling some periodic thing + return math.huge; + elseif type(value) == "number" then + -- assume seconds + ret = value; + elseif type(value) == "string" then + ret = human_io.parse_duration(value); + if value ~= nil and ret == nil then + ret = human_io.parse_duration_lax(value); + if ret then + local num = value:match("%d+"); + self:log("error", "Config option '%s' is set to ambiguous period '%s' - use full syntax e.g. '%s months' or '%s minutes'", name, value, num, num); + -- COMPAT: w/more relaxed behaviour in post-0.12 trunk. Return nil for this case too, eventually. + else + self:log("error", "Config option '%s' not understood, expecting a period (e.g. \"2 days\")", name); + return nil; + end + end + elseif value ~= nil then + self:log("error", "Config option '%s' expects a number or a period description string (e.g. \"3 hours\"), not %s", name, type(value)); + return nil; + else + return nil; + end + + if ret < 0 then + self:log("debug", "Treating negative period as infinity"); + return math.huge; + end + + if type(min) == "string" then + min = human_io.parse_duration(min); + end + if min and ret < min then + self:log("warn", "Config option '%s' out of bounds %g < %g", name, ret, min); + return min; + end + if type(max) == "string" then + max = human_io.parse_duration(max); + end + if max and ret > max then + self:log("warn", "Config option '%s' out of bounds %g > %g", name, ret, max); + return max; + end + return ret; end @@ -305,6 +390,15 @@ return resolve_relative_path(parent, value); end +function api:get_option_enum(name, default, ...) + local value = self:get_option_scalar(name, default); + if value == nil then return nil; end + local options = set.new{default, ...}; + if not options:contains(value) then + self:log("error", "Config option '%s' not in set of allowed values (one of: %s)", name, options); + end + return value; +end function api:context(host) return setmetatable({ host = host or "*", global = "*" == host }, { __index = self, __newindex = self }); @@ -328,7 +422,7 @@ end function api:get_host_items(key) - local modulemanager = require"core.modulemanager"; + local modulemanager = require"prosody.core.modulemanager"; local result = modulemanager.get_items(key, self.host) or {}; return result; end @@ -537,11 +631,12 @@ end function api:open_store(name, store_type) - return require"core.storagemanager".open(self.host, name or self.name, store_type); + if self.host == "*" then return nil, "global-storage-not-supported"; end + return require"prosody.core.storagemanager".open(self.host, name or self.name, store_type); end function api:measure(name, stat_type, conf) - local measure = require "core.statsmanager".measure; + local measure = require "prosody.core.statsmanager".measure; local fixed_label_key, fixed_label_value if self.host ~= "*" then fixed_label_key = "host" @@ -556,7 +651,7 @@ end function api:metric(type_, name, unit, description, label_keys, conf) - local metric = require "core.statsmanager".metric; + local metric = require "prosody.core.statsmanager".metric; local is_scoped = self.host ~= "*" if is_scoped then -- prepend `host` label to label keys if this is not a global module @@ -578,7 +673,7 @@ function api:set_status(status_type, status_message, override) local priority = status_priorities[status_type]; if not priority then - self:log("error", "set_status: Invalid status type '%s', assuming 'info'"); + self:log("error", "set_status: Invalid status type '%s', assuming 'info'", status_type); status_type, priority = "info", status_priorities.info; end local current_priority = status_priorities[self.status_type] or 0; @@ -601,4 +696,107 @@ return self.status_type, self.status_message, self.status_time; end +function api:default_permission(role_name, permission) + permission = permission:gsub("^:", self.name..":"); + if self.host == "*" then + for _, host in pairs(hosts) do + if host.authz then + host.authz.add_default_permission(role_name, permission); + end + end + return + end + hosts[self.host].authz.add_default_permission(role_name, permission); +end + +function api:default_permissions(role_name, permissions) + for _, permission in ipairs(permissions) do + self:default_permission(role_name, permission); + end +end + +function api:could(action, context) + return self:may(action, context, true); +end + +function api:may(action, context, peek) + if action:byte(1) == 58 then -- action begins with ':' + action = self.name..action; -- prepend module name + end + + do + -- JID-based actor + local actor_jid = type(context) == "string" and context or context.actor_jid; + if actor_jid then -- check JID permissions + local role; + local node, host = jid_split(actor_jid); + if host == self.host then + role = hosts[host].authz.get_user_role(node); + else + role = hosts[self.host].authz.get_jid_role(actor_jid); + end + if not role then + if not peek then + self:log("debug", "Access denied: JID <%s> may not %s (no role found)", actor_jid, action); + end + return false; + end + local permit = role:may(action); + if not permit then + if not peek then + self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", actor_jid, action, role.name); + end + end + return permit; + end + end + + -- Session-based actor + local session = context.origin or context.session; + if type(session) ~= "table" then + error("Unable to identify actor session from context"); + end + if session.type == "c2s" and session.host == self.host then + local role = session.role; + if not role then + if not peek then + self:log("warn", "Access denied: session %s has no role assigned"); + end + return false; + end + local permit = role:may(action, context); + if not permit and not peek then + self:log("debug", "Access denied: session %s (%s) may not %s (not permitted by role %s)", + session.id, session.full_jid, action, role.name + ); + end + return permit; + else + local actor_jid = context.stanza.attr.from; + local role = hosts[self.host].authz.get_jid_role(actor_jid); + if not role then + if not peek then + self:log("debug", "Access denied: JID <%s> may not %s (no role found)", actor_jid, action); + end + return false; + end + local permit = role:may(action, context); + if not permit and not peek then + self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", actor_jid, action, role.name); + end + return permit; + end +end + +-- Execute a function, once, but only after startup is complete +function api:on_ready(f) --luacheck: ignore 212/self + return prosody.started:next(f); +end + +-- COMPAT w/post 0.12 trunk +function api:once(f) + self:log("warn", "This module uses deprecated module:once() - switch to module:on_ready() or (better) expose function module.ready()"); + return self:on_ready(f); +end + return api; diff -r 54a936345aaa -r 5d9ec2e55d74 core/modulemanager.lua --- a/core/modulemanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/modulemanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,23 +6,23 @@ -- COPYING file in the source package for more information. -- -local array = require "util.array"; -local logger = require "util.logger"; +local array = require "prosody.util.array"; +local logger = require "prosody.util.logger"; local log = logger.init("modulemanager"); -local config = require "core.configmanager"; -local pluginloader = require "util.pluginloader"; -local envload = require "util.envload"; -local set = require "util.set"; +local config = require "prosody.core.configmanager"; +local pluginloader = require "prosody.util.pluginloader"; +local envload = require "prosody.util.envload"; +local set = require "prosody.util.set"; -local core_features = require "core.features".available; +local core_features = require "prosody.core.features".available; -local new_multitable = require "util.multitable".new; -local api = require "core.moduleapi"; -- Module API container +local new_multitable = require "prosody.util.multitable".new; +local api = require "prosody.core.moduleapi"; -- Module API container local prosody = prosody; local hosts = prosody.hosts; -local xpcall = require "util.xpcall".xpcall; +local xpcall = require "prosody.util.xpcall".xpcall; local debug_traceback = debug.traceback; local setmetatable, rawget = setmetatable, rawget; local ipairs, pairs, type, t_insert = ipairs, pairs, type, table.insert; @@ -293,6 +293,10 @@ ok, err = do_load_module(host, module_name); end end + + if module_has_method(pluginenv, "ready") then + pluginenv.module:on_ready(pluginenv.module.ready); + end end if not ok then modulemap[api_instance.host][module_name] = nil; diff -r 54a936345aaa -r 5d9ec2e55d74 core/portmanager.lua --- a/core/portmanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/portmanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,11 +1,11 @@ -local config = require "core.configmanager"; -local certmanager = require "core.certmanager"; -local server = require "net.server"; +local config = require "prosody.core.configmanager"; +local certmanager = require "prosody.core.certmanager"; +local server = require "prosody.net.server"; local socket = require "socket"; -local log = require "util.logger".init("portmanager"); -local multitable = require "util.multitable"; -local set = require "util.set"; +local log = require "prosody.util.logger".init("portmanager"); +local multitable = require "prosody.util.multitable"; +local set = require "prosody.util.set"; local table = table; local setmetatable, rawset, rawget = setmetatable, rawset, rawget; @@ -48,14 +48,11 @@ if err:match(" in use") then -- FIXME: Use service_name here if port == 5222 or port == 5223 or port == 5269 then - friendly_message = "check that Prosody or another XMPP server is " - .."not already running and using this port"; - elseif port == 80 or port == 81 then - friendly_message = "check that a HTTP server is not already using " - .."this port"; + friendly_message = "check that Prosody or another XMPP server is not already running and using this port"; + elseif port == 80 or port == 81 or port == 443 then + friendly_message = "check that a HTTP server is not already using this port"; elseif port == 5280 then - friendly_message = "check that Prosody or a BOSH connection manager " - .."is not already running"; + friendly_message = "check that Prosody or a BOSH connection manager is not already running"; else friendly_message = "this port is in use by another application"; end @@ -222,6 +219,13 @@ return data.service, data.server; end +local function get_tls_config_at(interface, port) + local data = active_services:search(nil, interface, port); + if not data or not data[1] or not data[1][1] then return nil, "not-found"; end + data = data[1][1]; + return data.tls_cfg; +end + local function get_service(service_name) return (services[service_name] or {})[1]; end @@ -240,21 +244,22 @@ log("debug", "Gathering certificates for SNI for host %s, %s service", host, service or "default"); for name, interface, port, n, active_service --luacheck: ignore 213 in active_services:iter(service, nil, nil, nil) do - if active_service.server.hosts and active_service.tls_cfg then - local config_prefix = (active_service.config_prefix or name).."_"; - if config_prefix == "_" then config_prefix = ""; end - local prefix_ssl_config = config.get(host, config_prefix.."ssl"); + if active_service.server and active_service.tls_cfg then local alternate_host = name and config.get(host, name.."_host"); if not alternate_host and name == "https" then -- TODO should this be some generic thing? e.g. in the service definition alternate_host = config.get(host, "http_host"); end local autocert = certmanager.find_host_cert(alternate_host or host); - -- luacheck: ignore 211/cfg - local ssl, err, cfg = certmanager.create_context(host, "server", prefix_ssl_config, autocert, active_service.tls_cfg); - if ssl then - active_service.server.hosts[alternate_host or host] = ssl; - else + local manualcert = active_service.tls_cfg; + local certificate = (autocert and autocert.certificate) or manualcert.certificate; + local key = (autocert and autocert.key) or manualcert.key; + local ok, err = active_service.server:sslctx():set_sni_host( + host, + certificate, + key + ); + if not ok then log("error", "Error creating TLS context for SNI host %s: %s", host, err); end end @@ -277,7 +282,7 @@ for name, interface, port, n, active_service --luacheck: ignore 213 in active_services:iter(nil, nil, nil, nil) do if active_service.tls_cfg then - active_service.server.hosts[host] = nil; + active_service.server:sslctx():remove_sni_host(host) end end end); @@ -312,6 +317,7 @@ unregister_service = unregister_service; close = close; get_service_at = get_service_at; + get_tls_config_at = get_tls_config_at; get_service = get_service; get_active_services = get_active_services; get_registered_services = get_registered_services; diff -r 54a936345aaa -r 5d9ec2e55d74 core/rostermanager.lua --- a/core/rostermanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/rostermanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,10 +9,10 @@ -local log = require "util.logger".init("rostermanager"); +local log = require "prosody.util.logger".init("rostermanager"); -local new_id = require "util.id".short; -local new_cache = require "util.cache".new; +local new_id = require "prosody.util.id".short; +local new_cache = require "prosody.util.cache".new; local pairs = pairs; local tostring = tostring; @@ -21,9 +21,9 @@ local hosts = prosody.hosts; local bare_sessions = prosody.bare_sessions; -local um_user_exists = require "core.usermanager".user_exists; -local st = require "util.stanza"; -local storagemanager = require "core.storagemanager"; +local um_user_exists = require "prosody.core.usermanager".user_exists; +local st = require "prosody.util.stanza"; +local storagemanager = require "prosody.core.storagemanager"; local _ENV = nil; -- luacheck: std none diff -r 54a936345aaa -r 5d9ec2e55d74 core/s2smanager.lua --- a/core/s2smanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/s2smanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -11,8 +11,8 @@ local hosts = prosody.hosts; local pairs, setmetatable = pairs, setmetatable; -local logger_init = require "util.logger".init; -local sessionlib = require "util.session"; +local logger_init = require "prosody.util.logger".init; +local sessionlib = require "prosody.util.session"; local log = logger_init("s2smanager"); diff -r 54a936345aaa -r 5d9ec2e55d74 core/sessionmanager.lua --- a/core/sessionmanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/sessionmanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,20 +10,20 @@ local tostring, setmetatable = tostring, setmetatable; local pairs, next= pairs, next; -local hosts = prosody.hosts; +local prosody, hosts = prosody, prosody.hosts; local full_sessions = prosody.full_sessions; local bare_sessions = prosody.bare_sessions; -local logger = require "util.logger"; +local logger = require "prosody.util.logger"; local log = logger.init("sessionmanager"); -local rm_load_roster = require "core.rostermanager".load_roster; -local config_get = require "core.configmanager".get; -local resourceprep = require "util.encodings".stringprep.resourceprep; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local generate_identifier = require "util.id".short; -local sessionlib = require "util.session"; +local rm_load_roster = require "prosody.core.rostermanager".load_roster; +local config_get = require "prosody.core.configmanager".get; +local resourceprep = require "prosody.util.encodings".stringprep.resourceprep; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local generate_identifier = require "prosody.util.id".short; +local sessionlib = require "prosody.util.session"; -local initialize_filters = require "util.filters".initialize; +local initialize_filters = require "prosody.util.filters".initialize; local gettime = require "socket".gettime; local _ENV = nil; @@ -92,6 +92,51 @@ return setmetatable(session, resting_session); end +-- Update a session with a new one (transplanting connection, filters, etc.) +-- new_session should be discarded after this call returns +local function update_session(to_session, from_session) + to_session.log("debug", "Updating with parameters from session %s", from_session.id); + from_session.log("debug", "Session absorbed into %s", to_session.id); + + local replaced_conn = to_session.conn; + if replaced_conn then + to_session.conn = nil; + end + + to_session.since = from_session.since; + to_session.ip = from_session.ip; + to_session.conn = from_session.conn; + to_session.rawsend = from_session.rawsend; + to_session.rawsend.session = to_session; + to_session.rawsend.conn = to_session.conn; + to_session.send = from_session.send; + to_session.send.session = to_session; + to_session.close = from_session.close; + to_session.filter = from_session.filter; + to_session.filter.session = to_session; + to_session.filters = from_session.filters; + to_session.send.filter = to_session.filter; + to_session.sasl_handler = from_session.sasl_handler; + to_session.stream = from_session.stream; + to_session.secure = from_session.secure; + to_session.hibernating = nil; + to_session.resumption_counter = (to_session.resumption_counter or 0) + 1; + from_session.log = to_session.log; + from_session.type = to_session.type; + -- Inform xmppstream of the new session (passed to its callbacks) + to_session.stream:set_session(to_session); + + -- Notify modules, allowing them to copy further fields or update state + prosody.events.fire_event("c2s-session-updated", { + session = to_session; + from_session = from_session; + replaced_conn = replaced_conn; + }); + + -- Retire the session we've pulled from, to avoid two sessions on the same connection + retire_session(from_session); +end + local function destroy_session(session, err) if session.destroyed then return; end @@ -130,15 +175,24 @@ retire_session(session); end -local function make_authenticated(session, username, scope) +local function make_authenticated(session, username, role_name) username = nodeprep(username); if not username or #username == 0 then return nil, "Invalid username"; end session.username = username; if session.type == "c2s_unauthed" then session.type = "c2s_unbound"; end - session.auth_scope = scope; - session.log("info", "Authenticated as %s@%s", username, session.host or "(unknown)"); + + local role; + if role_name then + role = hosts[session.host].authz.get_role_by_name(role_name); + else + role = hosts[session.host].authz.get_user_role(username); + end + if role then + sessionlib.set_role(session, role); + end + session.log("info", "Authenticated as %s@%s [%s]", username, session.host or "(unknown)", role and role.name or "no role"); return true; end @@ -265,6 +319,7 @@ return { new_session = new_session; retire_session = retire_session; + update_session = update_session; destroy_session = destroy_session; make_authenticated = make_authenticated; bind_resource = bind_resource; diff -r 54a936345aaa -r 5d9ec2e55d74 core/stanza_router.lua --- a/core/stanza_router.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/stanza_router.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,14 +6,14 @@ -- COPYING file in the source package for more information. -- -local log = require "util.logger".init("stanzarouter") +local log = require "prosody.util.logger".init("stanzarouter") local hosts = _G.prosody.hosts; local tostring = tostring; -local st = require "util.stanza"; -local jid_split = require "util.jid".split; -local jid_host = require "util.jid".host; -local jid_prepped_split = require "util.jid".prepped_split; +local st = require "prosody.util.stanza"; +local jid_split = require "prosody.util.jid".split; +local jid_host = require "prosody.util.jid".host; +local jid_prepped_split = require "prosody.util.jid".prepped_split; local full_sessions = _G.prosody.full_sessions; local bare_sessions = _G.prosody.bare_sessions; @@ -127,7 +127,7 @@ end core_post_stanza(origin, stanza, origin.full_jid); else - local h = hosts[stanza.attr.to or origin.host or origin.to_host]; + local h = hosts[stanza.attr.to or origin.host]; if h then local event; if xmlns == nil then @@ -143,7 +143,7 @@ if h.events.fire_event(event, {origin = origin, stanza = stanza}) then return; end end if host and not hosts[host] then host = nil; end -- COMPAT: workaround for a Pidgin bug which sets 'to' to the SRV result - handle_unhandled_stanza(host or origin.host or origin.to_host, origin, stanza); + handle_unhandled_stanza(host or origin.host, origin, stanza); end end diff -r 54a936345aaa -r 5d9ec2e55d74 core/statsmanager.lua --- a/core/statsmanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/statsmanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,10 @@ -local config = require "core.configmanager"; -local log = require "util.logger".init("stats"); -local timer = require "util.timer"; +local config = require "prosody.core.configmanager"; +local log = require "prosody.util.logger".init("stats"); +local timer = require "prosody.util.timer"; local fire_event = prosody.events.fire_event; -local array = require "util.array"; -local timed = require "util.openmetrics".timed; +local array = require "prosody.util.array"; +local timed = require "prosody.util.openmetrics".timed; local stats_interval_config = config.get("*", "statistics_interval"); local stats_interval = tonumber(stats_interval_config); @@ -26,8 +26,8 @@ end local builtin_providers = { - internal = "util.statistics"; - statsd = "util.statsd"; + internal = "prosody.util.statistics"; + statsd = "prosody.util.statsd"; }; diff -r 54a936345aaa -r 5d9ec2e55d74 core/storagemanager.lua --- a/core/storagemanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/storagemanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -3,12 +3,12 @@ local setmetatable = setmetatable; local rawset = rawset; -local config = require "core.configmanager"; -local datamanager = require "util.datamanager"; -local modulemanager = require "core.modulemanager"; -local multitable = require "util.multitable"; -local log = require "util.logger".init("storagemanager"); -local async = require "util.async"; +local config = require "prosody.core.configmanager"; +local datamanager = require "prosody.util.datamanager"; +local modulemanager = require "prosody.core.modulemanager"; +local multitable = require "prosody.util.multitable"; +local log = require "prosody.util.logger".init("storagemanager"); +local async = require "prosody.util.async"; local debug = debug; local prosody = prosody; @@ -91,24 +91,8 @@ end local function get_storage_config(host) - -- COMPAT w/ unreleased Prosody 0.10 and the once-experimental mod_storage_sql2 in peoples' config files - local storage_config = config.get(host, "storage"); - local found_sql2; - if storage_config == "sql2" then - storage_config, found_sql2 = "sql", true; - elseif type(storage_config) == "table" then - for store_name, driver_name in pairs(storage_config) do - if driver_name == "sql2" then - storage_config[store_name] = "sql"; - found_sql2 = true; - end - end - end - if found_sql2 then - log("error", "The temporary 'sql2' storage module has now been renamed to 'sql', " - .."please update your config file: https://prosody.im/doc/modules/mod_storage_sql2"); - end - return storage_config; + -- Here used to be some some compat checks + return config.get(host, "storage"); end local function get_driver(host, store) @@ -203,6 +187,37 @@ }; } +local combined_store_mt = { + __index = { + -- keyval + get = function (self, name) + return self.keyval_store:get(name); + end; + set = function (self, name, data) + return self.keyval_store:set(name, data); + end; + items = function (self) + return self.keyval_store:users(); + end; + -- map + get_key = function (self, name, key) + return self.map_store:get(name, key); + end; + set_key = function (self, name, key, value) + return self.map_store:set(name, key, value); + end; + set_keys = function (self, name, map) + return self.map_store:set_keys(name, map); + end; + get_key_from_all = function (self, key) + return self.map_store:get_all(key); + end; + delete_key_from_all = function (self, key) + return self.map_store:delete_all(key); + end; + }; +}; + local open; -- forward declaration local function create_map_shim(host, store) @@ -213,7 +228,49 @@ }, map_shim_mt); end +local function open_combined(host, store) + local driver, driver_name = get_driver(host, store); + + -- Open keyval + local keyval_store, err = driver:open(store, "keyval"); + if not keyval_store then + if err == "unsupported-store" then + log("debug", "Storage driver %s does not support store %s (keyval), falling back to null driver", + driver_name, store); + keyval_store, err = null_storage_driver, nil; + end + end + + local map_store; + if keyval_store then + -- Open map + map_store, err = driver:open(store, "map"); + if not map_store then + if err == "unsupported-store" then + log("debug", "Storage driver %s does not support store %s (map), falling back to shim", + driver_name, store); + map_store, err = setmetatable({ keyval_store = keyval_store }, map_shim_mt), nil; + end + end + end + + if not(keyval_store and map_store) then + return nil, err; + end + local combined_store = setmetatable({ + keyval_store = keyval_store; + map_store = map_store; + remove = map_store.remove; + }, combined_store_mt); + local event_data = { host = host, store_name = store, store_type = "keyval+", store = combined_store }; + hosts[host].events.fire_event("store-opened", event_data); + return event_data.store, event_data.store_err; +end + function open(host, store, typ) + if typ == "keyval+" then -- TODO: default in some release? + return open_combined(host, store); + end local driver, driver_name = get_driver(host, store); local ret, err = driver:open(store, typ); if not ret then diff -r 54a936345aaa -r 5d9ec2e55d74 core/usermanager.lua --- a/core/usermanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/core/usermanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,17 +6,13 @@ -- COPYING file in the source package for more information. -- -local modulemanager = require "core.modulemanager"; -local log = require "util.logger".init("usermanager"); +local modulemanager = require "prosody.core.modulemanager"; +local log = require "prosody.util.logger".init("usermanager"); local type = type; -local it = require "util.iterators"; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; -local jid_prep = require "util.jid".prep; -local config = require "core.configmanager"; -local sasl_new = require "util.sasl".new; -local storagemanager = require "core.storagemanager"; -local set = require "util.set"; +local jid_split = require "prosody.util.jid".split; +local config = require "prosody.core.configmanager"; +local sasl_new = require "prosody.util.sasl".new; +local storagemanager = require "prosody.core.storagemanager"; local prosody = _G.prosody; local hosts = prosody.hosts; @@ -25,6 +21,8 @@ local default_provider = "internal_hashed"; +local debug = debug; + local _ENV = nil; -- luacheck: std none @@ -36,26 +34,26 @@ }); end -local global_admins_config = config.get("*", "admins"); -if type(global_admins_config) ~= "table" then - global_admins_config = nil; -- TODO: factor out moduleapi magic config handling and use it here -end -local global_admins = set.new(global_admins_config) / jid_prep; +local fallback_authz_provider = { + -- luacheck: ignore 212 + get_jids_with_role = function (role) end; + + get_user_role = function (user) end; + set_user_role = function (user, role_name) end; -local admin_role = { ["prosody:admin"] = true }; -local global_authz_provider = { - get_user_roles = function (user) end; --luacheck: ignore 212/user - get_jid_roles = function (jid) - if global_admins:contains(jid) then - return admin_role; - end - end; - get_jids_with_role = function (role) - if role ~= "prosody:admin" then return {}; end - return it.to_array(global_admins); - end; - set_user_roles = function (user, roles) end; -- luacheck: ignore 212 - set_jid_roles = function (jid, roles) end; -- luacheck: ignore 212 + get_user_secondary_roles = function (user) end; + add_user_secondary_role = function (user, host, role_name) end; + remove_user_secondary_role = function (user, host, role_name) end; + + user_can_assume_role = function(user, role_name) end; + + get_jid_role = function (jid) end; + set_jid_role = function (jid, role) end; + + get_users_with_role = function (role_name) end; + add_default_permission = function (role_name, action, policy) end; + get_role_by_name = function (role_name) end; + get_all_roles = function () end; }; local provider_mt = { __index = new_null_provider() }; @@ -66,7 +64,7 @@ local authz_provider_name = config.get(host, "authorization") or "internal"; local authz_mod = modulemanager.load(host, "authz_"..authz_provider_name); - host_session.authz = authz_mod or global_authz_provider; + host_session.authz = authz_mod or fallback_authz_provider; if host_session.type ~= "local" then return; end @@ -116,6 +114,12 @@ return ok, err; end +local function get_account_info(username, host) + local method = hosts[host].users.get_account_info; + if not method then return nil, "method not supported"; end + return method(username); +end + local function user_exists(username, host) if hosts[host].sessions[username] then return true; end return hosts[host].users.user_exists(username); @@ -132,6 +136,43 @@ return storagemanager.purge(username, host); end +local function user_is_enabled(username, host) + local method = hosts[host].users.is_enabled; + if method then return method(username); end + + -- Fallback + local info, err = get_account_info(username, host); + if info and info.enabled ~= nil then + return info.enabled; + elseif err ~= "method not implemented" then + -- Storage issues etetc + return info, err; + end + + -- API unsupported implies users are always enabled + return true; +end + +local function enable_user(username, host) + local method = hosts[host].users.enable; + if not method then return nil, "method not supported"; end + local ret, err = method(username); + if ret then + prosody.events.fire_event("user-enabled", { username = username, host = host }); + end + return ret, err; +end + +local function disable_user(username, host, meta) + local method = hosts[host].users.disable; + if not method then return nil, "method not supported"; end + local ret, err = method(username, meta); + if ret then + prosody.events.fire_event("user-disabled", { username = username, host = host, meta = meta }); + end + return ret, err; +end + local function users(host) return hosts[host].users.users(); end @@ -144,70 +185,143 @@ return hosts[host].users; end -local function get_roles(jid, host) +local function get_user_role(user, host) if host and not hosts[host] then return false; end - if type(jid) ~= "string" then return false; end + if type(user) ~= "string" then return false; end - jid = jid_bare(jid); - host = host or "*"; + return hosts[host].authz.get_user_role(user); +end + +local function set_user_role(user, host, role_name) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end - local actor_user, actor_host = jid_split(jid); - local roles; - - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + local role, err = hosts[host].authz.set_user_role(user, role_name); + if role then + prosody.events.fire_event("user-role-changed", { + username = user, host = host, role = role; + }); + end + return role, err; +end - if actor_user and actor_host == host then -- Local user - roles = authz_provider.get_user_roles(actor_user); - else -- Remote user/JID - roles = authz_provider.get_jid_roles(jid); +local function create_user_with_role(username, password, host, role) + local ok, err = create_user(username, nil, host); + if not ok then return ok, err; end + + local role_ok, role_err = set_user_role(username, host, role); + if not role_ok then + delete_user(username, host); + return nil, "Failed to assign role: "..role_err; end - return roles; + if password then + local pw_ok, pw_err = set_password(username, password, host); + if not pw_ok then + return nil, "Failed to set password: "..pw_err; + end + + local enable_ok, enable_err = enable_user(username, host); + if not enable_ok and enable_err ~= "method not implemented" then + return enable_ok, "Failed to enable account: "..enable_err; + end + end + + return true; +end + +local function user_can_assume_role(user, host, role_name) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + return hosts[host].authz.user_can_assume_role(user, role_name); end -local function set_roles(jid, host, roles) +local function add_user_secondary_role(user, host, role_name) if host and not hosts[host] then return false; end - if type(jid) ~= "string" then return false; end - - jid = jid_bare(jid); - host = host or "*"; - - local actor_user, actor_host = jid_split(jid); + if type(user) ~= "string" then return false; end - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - if actor_user and actor_host == host then -- Local user - local ok, err = authz_provider.set_user_roles(actor_user, roles); - if ok then - prosody.events.fire_event("user-roles-changed", { - username = actor_user, host = actor_host - }); - end - return ok, err; - else -- Remote entity - return authz_provider.set_jid_roles(jid, roles) + local role, err = hosts[host].authz.add_user_secondary_role(user, role_name); + if role then + prosody.events.fire_event("user-role-added", { + username = user, host = host, role = role; + }); end + return role, err; +end + +local function remove_user_secondary_role(user, host, role_name) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + local ok, err = hosts[host].authz.remove_user_secondary_role(user, role_name); + if ok then + prosody.events.fire_event("user-role-removed", { + username = user, host = host, role_name = role_name; + }); + end + return ok, err; end +local function get_user_secondary_roles(user, host) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + return hosts[host].authz.get_user_secondary_roles(user); +end + +local function get_jid_role(jid, host) + local jid_node, jid_host = jid_split(jid); + if host == jid_host and jid_node then + return hosts[host].authz.get_user_role(jid_node); + end + return hosts[host].authz.get_jid_role(jid); +end + +local function set_jid_role(jid, host, role_name) + local _, jid_host = jid_split(jid); + if host == jid_host then + return nil, "unexpected-local-jid"; + end + return hosts[host].authz.set_jid_role(jid, role_name) +end + +local strict_deprecate_is_admin; +local legacy_admin_roles = { ["prosody:admin"] = true, ["prosody:operator"] = true }; local function is_admin(jid, host) - local roles = get_roles(jid, host); - return roles and roles["prosody:admin"]; + if strict_deprecate_is_admin == nil then + strict_deprecate_is_admin = (config.get("*", "strict_deprecate_is_admin") == true); + end + if strict_deprecate_is_admin then + log("error", "Attempt to use deprecated is_admin() API: %s", debug.traceback()); + return false; + end + log("warn", "Usage of legacy is_admin() API, which will be disabled in a future build: %s", debug.traceback()); + log("warn", "See https://prosody.im/doc/developers/permissions about the new permissions API"); + return legacy_admin_roles[get_jid_role(jid, host)] or false; end local function get_users_with_role(role, host) if not hosts[host] then return false; end if type(role) ~= "string" then return false; end - return hosts[host].authz.get_users_with_role(role); end local function get_jids_with_role(role, host) if host and not hosts[host] then return false; end if type(role) ~= "string" then return false; end - - host = host or "*"; + return hosts[host].authz.get_jids_with_role(role); +end - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - return authz_provider.get_jids_with_role(role); +local function get_role_by_name(role_name, host) + if host and not hosts[host] then return false; end + if type(role_name) ~= "string" then return false; end + return hosts[host].authz.get_role_by_name(role_name); +end + +local function get_all_roles(host) + if host and not hosts[host] then return false; end + return hosts[host].authz.get_all_roles(); end return { @@ -216,15 +330,30 @@ test_password = test_password; get_password = get_password; set_password = set_password; + get_account_info = get_account_info; user_exists = user_exists; create_user = create_user; + create_user_with_role = create_user_with_role; delete_user = delete_user; + user_is_enabled = user_is_enabled; + enable_user = enable_user; + disable_user = disable_user; users = users; get_sasl_handler = get_sasl_handler; get_provider = get_provider; - get_roles = get_roles; - set_roles = set_roles; + get_user_role = get_user_role; + set_user_role = set_user_role; + user_can_assume_role = user_can_assume_role; + add_user_secondary_role = add_user_secondary_role; + remove_user_secondary_role = remove_user_secondary_role; + get_user_secondary_roles = get_user_secondary_roles; + get_users_with_role = get_users_with_role; + get_jid_role = get_jid_role; + set_jid_role = set_jid_role; + get_jids_with_role = get_jids_with_role; + get_role_by_name = get_role_by_name; + get_all_roles = get_all_roles; + + -- Deprecated is_admin = is_admin; - get_users_with_role = get_users_with_role; - get_jids_with_role = get_jids_with_role; }; diff -r 54a936345aaa -r 5d9ec2e55d74 doc/doap.xml --- a/doc/doap.xml Wed Mar 27 15:35:15 2024 +0000 +++ b/doc/doap.xml Wed Mar 27 15:39:03 2024 +0000 @@ -56,18 +56,23 @@ + + + + + - 2.12.1 + 2.13.0 0.4.0 partial no support for multiple items (reported tag) @@ -119,7 +124,7 @@ - 1.34.1 + 1.34.5 0.3.0 partial @@ -172,7 +177,7 @@ - 1.22.0 + 1.24.1 0.9.0 partial mod_pubsub @@ -240,7 +245,8 @@ 1.2 0.1.0 - complete + trunk + removed mod_time @@ -268,6 +274,7 @@ 1.0 0.9.0 complete + util.jid.(un)escape, missing rejection of \20 at start or end per xep version 1.1 @@ -297,7 +304,7 @@ - 1.5.2 + 1.6.0 0.8.0 complete @@ -355,7 +362,7 @@ - 2.0 + 2.1 0.6.0 0.10.0 removed @@ -390,7 +397,7 @@ - 1.2.1 + 1.2.2 0.5.0 complete @@ -561,7 +568,7 @@ - 0.7.1 + 1.0.0 complete 0.12.0 mod_external_services @@ -623,7 +630,7 @@ - 1.0.0 + 1.0.1 complete 0.10.0 @@ -657,7 +664,7 @@ - 1.0.0 + 1.0 0.11.0 complete Used by XEP-0280, XEP-0313 @@ -683,7 +690,7 @@ - 1.0.0 + 1.1.0 complete 0.10.0 mod_mam, mod_muc_mam @@ -719,7 +726,7 @@ - 0.4.0 + 0.6.0 0.11.6 complete triggers buffer flush in mod_csi_simple since 0.11.6; recognised by mod_carbons and mod_mam since 0.12 @@ -728,7 +735,7 @@ - 0.6.1 + 0.7.0 complete 0.10.0 Used in context of XEP-0313 by mod_mam and mod_muc_mam @@ -737,7 +744,7 @@ - 1.0.0 + 1.1.0 complete 0.12.0 mod_http_file_share @@ -763,7 +770,7 @@ - 0.3.0 + 0.4.0 0.11.0 complete Used in context of XEP-0352 @@ -772,7 +779,7 @@ - 0.8.1 + 0.8.3 complete via XEP-0163, XEP-0222 @@ -789,7 +796,7 @@ - 0.3.0 + 0.5.0 0.12.0 partial @@ -839,11 +846,34 @@ + + 0.4.0 + trunk + complete + + + + 0.2.0 complete Broken out of XEP-0313 + + + + 0.2.0 + 0.12.0 + complete + + + + + + 0.1.0 + trunk + + diff -r 54a936345aaa -r 5d9ec2e55d74 doc/hgrc-email.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/hgrc-email.ini Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,16 @@ +# Settings for using `hg email` + +[email] +# See `hg help config.email` +to = prosody-dev@googlegroups.com + +[smtp] +# See `hg help config.smtp` +host = mail.example.com +port = submission +tls = starttls +username = + +[extensions] +# command to send changesets as (a series of) patch emails +patchbomb = diff -r 54a936345aaa -r 5d9ec2e55d74 doc/hgrc.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/hgrc.ini Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,43 @@ +[paths] +default = https://hg.prosody.im/trunk/ +default:pushrev = . +default:pushurl = ssh://hg@hg.prosody.im/prosody-hg/ + +contrib = https://hg.prosody.im/contrib/ +contrib:bookmarks.mode = ignore +contrib:pushrev = . +contrib:pushurl = ssh://hg@hg.prosody.im/contrib/ + +[ui] + +# The Mercurial project recommends enabling tweakdefaults to get slight +# improvements to the UI over time. Make sure to set HGPLAIN in the environment +# when writing scripts! +tweakdefaults = True + +[phases] +# Disable marking changesets as published when pushing to a local repository +publish = False + +[revsetalias] +# Convenient alias to find current trunk revision +trunk = last(public() and branch("default")) +[experimental] +# Require changes to have a topic branch +topic-mode = enforce + +[fix] +trailing-whitespace:command = sed +trailing-whitespace:linerange = -e '{first},{last}s/\s\+$//' +trailing-whitespace:pattern = set:not binary() + +astyle:command = astyle --indent=tab --attach-classes --indent-switches --break-blocks --pad-oper --unpad-paren --add-braces --align-pointer=name --lineend=linux +astyle:pattern = set:**.c +json:command = json_pp -json_opt canonical,pretty +json:pattern = set:**.json + +[extensions] +# The Mercurial Changeset Evolution plugin is strongly recommended +evolve = +# support for topic branches +topic = diff -r 54a936345aaa -r 5d9ec2e55d74 doc/storage.tld --- a/doc/storage.tld Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,68 +0,0 @@ --- Storage Interface API Description --- --- This is written as a TypedLua description - --- Key-Value stores (the default) - -interface keyval_store - get : ( self, string? ) -> (any) | (nil, string) - set : ( self, string?, any ) -> (boolean) | (nil, string) -end - --- Map stores (key-key-value stores) - -interface map_store - get : ( self, string?, any ) -> (any) | (nil, string) - set : ( self, string?, any, any ) -> (boolean) | (nil, string) - set_keys : ( self, string?, { any : any }) -> (boolean) | (nil, string) - remove : {} -end - --- Archive stores - -typealias archive_query = { - "start" : number?, -- timestamp - "end" : number?, -- timestamp - "with" : string?, - "after" : string?, -- archive id - "before" : string?, -- archive id - "total" : boolean?, -} - -interface archive_store - -- Optional set of capabilities - caps : { - -- Optional total count of matching items returned as second return value from :find() - "total" : boolean?, - }? - - -- Add to the archive - append : ( self, string?, string?, any, number?, string? ) -> (string) | (nil, string) - - -- Iterate over archive - find : ( self, string?, archive_query? ) -> ( () -> ( string, any, number?, string? ), integer? ) - - -- Removal of items. API like find. Optional? - delete : ( self, string?, archive_query? ) -> (boolean) | (number) | (nil, string) - - -- Array of dates which do have messages (Optional?) - dates : ( self, string? ) -> ({ string }) | (nil, string) - - -- Map of counts per "with" field - summary : ( self, string?, archive_query? ) -> ( { string : integer } ) | (nil, string) - - -- Map-store API - get : ( self, string, string ) -> (stanza, number?, string?) | (nil, string) - set : ( self, string, string, stanza, number?, string? ) -> (boolean) | (nil, string) -end - --- This represents moduleapi -interface module - -- If the first string is omitted then the name of the module is used - -- The second string is one of "keyval" (default), "map" or "archive" - open_store : (self, string?, string?) -> (keyval_store) | (map_store) | (archive_store) | (nil, string) - - -- Other module methods omitted -end - -module : module diff -r 54a936345aaa -r 5d9ec2e55d74 loader.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/loader.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,35 @@ +-- Allow for both require"util.foo" and require"prosody.util.foo" for a +-- transition period while we update all require calls. + +if (...) == "prosody.loader" then + if not package.path:find "prosody" then + -- For require"util.foo" also look in paths equivalent to "prosody.util.foo" + package.path = package.path:gsub("([^;]*)(?[^;]*)", "%1prosody/%2;%1%2"); + package.cpath = package.cpath:gsub("([^;]*)(?[^;]*)", "%1prosody/%2;%1%2"); + end +else + -- When requiring "prosody.x", also look for "x" + for i = #package.searchers, 1, -1 do + local search = package.searchers[i]; + table.insert(package.searchers, i, function(module_name) + local lib = module_name:match("^prosody%.(.*)$"); + if lib then + return search(lib); + end + end) + end +end + +-- Look for already loaded module with or without prefix +setmetatable(package.loaded, { + __index = function(loaded, module_name) + local suffix = module_name:match("^prosody%.(.*)$"); + if suffix then + return rawget(loaded, suffix); + end + local prefixed = rawget(loaded, "prosody." .. module_name); + if prefixed ~= nil then + return prefixed; + end + end; +}) diff -r 54a936345aaa -r 5d9ec2e55d74 makefile --- a/makefile Wed Mar 27 15:35:15 2024 +0000 +++ b/makefile Wed Mar 27 15:39:03 2024 +0000 @@ -44,6 +44,10 @@ $(INSTALL_EXEC) ./prosody.install $(BIN)/prosody $(INSTALL_EXEC) ./prosodyctl.install $(BIN)/prosodyctl +install-loader: + $(MKDIR) $(SOURCE) + $(INSTALL_DATA) loader.lua $(SOURCE) + install-core: $(MKDIR) $(SOURCE) $(MKDIR) $(SOURCE)/core @@ -73,12 +77,13 @@ install-plugins: $(MKDIR) $(MODULES) - $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam + $(MKDIR) $(MODULES)/mod_pubsub $(MODULES)/adhoc $(MODULES)/muc $(MODULES)/mod_mam $(MODULES)/mod_debug_stanzas $(INSTALL_DATA) plugins/*.lua $(MODULES) $(INSTALL_DATA) plugins/mod_pubsub/*.lua $(MODULES)/mod_pubsub $(INSTALL_DATA) plugins/adhoc/*.lua $(MODULES)/adhoc $(INSTALL_DATA) plugins/muc/*.lua $(MODULES)/muc $(INSTALL_DATA) plugins/mod_mam/*.lua $(MODULES)/mod_mam + $(INSTALL_DATA) plugins/mod_debug_stanzas/*.lua $(MODULES)/mod_debug_stanzas install-man: $(MKDIR) $(MAN)/man1 @@ -90,7 +95,7 @@ install-data: $(MKDIR_PRIVATE) $(DATA) -install: install-util install-net install-core install-plugins install-bin install-etc install-man install-meta install-data +install: install-util install-net install-core install-plugins install-bin install-etc install-man install-meta install-data install-loader clean: rm -f prosody.install diff -r 54a936345aaa -r 5d9ec2e55d74 net/adns.lua --- a/net/adns.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/adns.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,11 +6,11 @@ -- COPYING file in the source package for more information. -- -local server = require "net.server"; -local new_resolver = require "net.dns".resolver; -local promise = require "util.promise"; +local server = require "prosody.net.server"; +local new_resolver = require "prosody.net.dns".resolver; +local promise = require "prosody.util.promise"; -local log = require "util.logger".init("adns"); +local log = require "prosody.util.logger".init("adns"); log("debug", "Using legacy DNS API (missing lua-unbound?)"); -- TODO write docs about luaunbound -- TODO Raise log level once packages are available diff -r 54a936345aaa -r 5d9ec2e55d74 net/connect.lua --- a/net/connect.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/connect.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,8 +1,8 @@ -local server = require "net.server"; -local log = require "util.logger".init("net.connect"); -local new_id = require "util.id".short; +local server = require "prosody.net.server"; +local log = require "prosody.util.logger".init("net.connect"); +local new_id = require "prosody.util.id".short; +local timer = require "prosody.util.timer"; --- TODO #1246 Happy Eyeballs -- FIXME RFC 6724 -- FIXME Error propagation from resolvers doesn't work -- FIXME #1428 Reuse DNS resolver object between service and basic resolver @@ -28,16 +28,17 @@ local function attempt_connection(p) p:log("debug", "Checking for targets..."); - if p.conn then - pending_connections_map[p.conn] = nil; - p.conn = nil; - end - p.target_resolver:next(function (conn_type, ip, port, extra) + p.target_resolver:next(function (conn_type, ip, port, extra, more_targets_available) if not conn_type then -- No more targets to try p:log("debug", "No more connection targets to try", p.target_resolver.last_error); - if p.listeners.onfail then - p.listeners.onfail(p.data, p.last_error or p.target_resolver.last_error or "unable to resolve service"); + if next(p.conns) == nil then + p:log("debug", "No more targets, no pending connections. Connection failed."); + if p.listeners.onfail then + p.listeners.onfail(p.data, p.last_error or p.target_resolver.last_error or "unable to resolve service"); + end + else + p:log("debug", "One or more connection attempts are still pending. Waiting for now."); end return; end @@ -49,8 +50,16 @@ p.last_error = err or "unknown reason"; return attempt_connection(p); end - p.conn = conn; + p.conns[conn] = true; pending_connections_map[conn] = p; + if more_targets_available then + timer.add_task(0.250, function () + if not p.connected then + p:log("debug", "Still not connected, making parallel connection attempt..."); + attempt_connection(p); + end + end); + end end); end @@ -62,6 +71,13 @@ return; end pending_connections_map[conn] = nil; + if p.connected then + -- We already succeeded in connecting + p.conns[conn] = nil; + conn:close(); + return; + end + p.connected = true; p:log("debug", "Successfully connected"); conn:setlistener(p.listeners, p.data); return p.listeners.onconnect(conn); @@ -73,9 +89,18 @@ log("warn", "Failed connection, but unexpected!"); return; end + p.conns[conn] = nil; + pending_connections_map[conn] = nil; p.last_error = reason or "unknown reason"; p:log("debug", "Connection attempt failed: %s", p.last_error); - attempt_connection(p); + if p.connected then + p:log("debug", "Connection already established, ignoring failure"); + elseif next(p.conns) == nil then + p:log("debug", "No pending connection attempts, and not yet connected"); + attempt_connection(p); + else + p:log("debug", "Other attempts are still pending, ignoring failure"); + end end local function connect(target_resolver, listeners, options, data) @@ -85,6 +110,7 @@ listeners = assert(listeners); options = options or {}; data = data; + conns = {}; }, pending_connection_mt); p:log("debug", "Starting connection process"); diff -r 54a936345aaa -r 5d9ec2e55d74 net/cqueues.lua --- a/net/cqueues.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/cqueues.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,9 +7,9 @@ -- This module allows you to use cqueues with a net.server mainloop -- -local server = require "net.server"; +local server = require "prosody.net.server"; local cqueues = require "cqueues"; -local timer = require "util.timer"; +local timer = require "prosody.util.timer"; assert(cqueues.VERSION >= 20150113, "cqueues newer than 20150113 required") -- Create a single top level cqueue diff -r 54a936345aaa -r 5d9ec2e55d74 net/dns.lua --- a/net/dns.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/dns.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,18 +8,18 @@ -- todo: cache results of encodeName --- reference: http://tools.ietf.org/html/rfc1035 --- reference: http://tools.ietf.org/html/rfc1876 (LOC) +-- reference: https://www.rfc-editor.org/rfc/rfc1035.html +-- reference: https://www.rfc-editor.org/rfc/rfc1876.html (LOC) local socket = require "socket"; -local have_timer, timer = pcall(require, "util.timer"); -local new_ip = require "util.ip".new_ip; -local have_util_net, util_net = pcall(require, "util.net"); +local have_timer, timer = pcall(require, "prosody.util.timer"); +local new_ip = require "prosody.util.ip".new_ip; +local have_util_net, util_net = pcall(require, "prosody.util.net"); -local log = require "util.logger".init("dns"); +local log = require "prosody.util.logger".init("dns"); -local _, windows = pcall(require, "util.windows"); +local _, windows = pcall(require, "prosody.util.windows"); local is_windows = (_ and windows) or os.getenv("WINDIR"); local coroutine, io, math, string, table = diff -r 54a936345aaa -r 5d9ec2e55d74 net/http.lua --- a/net/http.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/http.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,17 +6,17 @@ -- COPYING file in the source package for more information. -- -local b64 = require "util.encodings".base64.encode; +local b64 = require "prosody.util.encodings".base64.encode; local url = require "socket.url" -local httpstream_new = require "net.http.parser".new; -local util_http = require "util.http"; -local events = require "util.events"; -local verify_identity = require"util.x509".verify_identity; -local promise = require "util.promise"; -local http_errors = require "net.http.errors"; +local httpstream_new = require "prosody.net.http.parser".new; +local util_http = require "prosody.util.http"; +local events = require "prosody.util.events"; +local verify_identity = require"prosody.util.x509".verify_identity; +local promise = require "prosody.util.promise"; +local http_errors = require "prosody.net.http.errors"; -local basic_resolver = require "net.resolvers.basic"; -local connect = require "net.connect".connect; +local basic_resolver = require "prosody.net.resolvers.basic"; +local connect = require "prosody.net.connect".connect; local ssl_available = pcall(require, "ssl"); @@ -25,10 +25,10 @@ local tonumber, tostring, traceback = tonumber, tostring, debug.traceback; local os_time = os.time; -local xpcall = require "util.xpcall".xpcall; +local xpcall = require "prosody.util.xpcall".xpcall; local error = error -local log = require "util.logger".init("http"); +local log = require "prosody.util.logger".init("http"); local _ENV = nil; -- luacheck: std none @@ -51,10 +51,19 @@ return ...; end -local function destroy_request(request) +local function destroy_request(request, force) local conn = request.conn; if conn then request.conn = nil; + local pool = request.http.pool; + if pool and not force then + local pool_id = request.scheme .. "://" .. request.authority; + if not pool[pool_id] then + pool[conn] = pool_id; + pool[pool_id] = conn; + return; + end + end conn:close() end end @@ -106,7 +115,8 @@ request.callback(r.body, r.code, r, request); request.callback = nil; end - destroy_request(request); + local persistent = (","..(r.headers.connection or "keep-alive")..","):find(",keep-alive,") + destroy_request(request, persistent); end local function options_cb() return request; @@ -193,6 +203,13 @@ if request and request.conn then request:reader(nil, err or "closed"); end + if request and request.http.pool then + local pool = request.http.pool; + local pool_id = pool[conn]; + if pool_id then + pool[pool_id], pool[conn] = nil, nil; + end + end requests[conn] = nil; end @@ -253,6 +270,12 @@ ["User-Agent"] = "Prosody XMPP Server"; }; + if self.pool then + headers["Connection"] = "keep-alive"; + else + headers["Connection"] = "close"; + end + if req.userinfo then headers["Authorization"] = "Basic "..b64(req.userinfo); end @@ -296,6 +319,23 @@ end end + if self.pool then + local pool_id = req.scheme .. "://" .. req.authority; + local conn = self.pool[pool_id]; + if conn then + log("debug", "Re-using connection to %s from pool", req.host); + self.pool[pool_id] = nil; + self.pool[conn] = nil; + req.conn = conn; + requests[conn] = req; + self.events.fire_event("request", { http = self, request = req, url = u }); + listener.onconnect(conn); + return req; + else + log("debug", "Opening a new connection for this request"); + end + end + local http_service = basic_resolver.new(host, port_number, "tcp", { servername = req.host; use_dane = use_dane }); connect(http_service, listener, { sslctx = sslctx }, req); @@ -332,6 +372,10 @@ end or new; events = events.new(); }; + if options and options.connection_pooling then + -- util.cache in the future? + http.pool = {}; + end return http; end diff -r 54a936345aaa -r 5d9ec2e55d74 net/http/codes.lua --- a/net/http/codes.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/http/codes.lua Wed Mar 27 15:39:03 2024 +0000 @@ -2,62 +2,62 @@ local response_codes = { -- Source: http://www.iana.org/assignments/http-status-codes - [100] = "Continue"; -- RFC7231, Section 6.2.1 - [101] = "Switching Protocols"; -- RFC7231, Section 6.2.2 + [100] = "Continue"; -- RFC9110, Section 15.2.1 + [101] = "Switching Protocols"; -- RFC9110, Section 15.2.2 [102] = "Processing"; [103] = "Early Hints"; -- [104-199] = "Unassigned"; - [200] = "OK"; -- RFC7231, Section 6.3.1 - [201] = "Created"; -- RFC7231, Section 6.3.2 - [202] = "Accepted"; -- RFC7231, Section 6.3.3 - [203] = "Non-Authoritative Information"; -- RFC7231, Section 6.3.4 - [204] = "No Content"; -- RFC7231, Section 6.3.5 - [205] = "Reset Content"; -- RFC7231, Section 6.3.6 - [206] = "Partial Content"; -- RFC7233, Section 4.1 + [200] = "OK"; -- RFC9110, Section 15.3.1 + [201] = "Created"; -- RFC9110, Section 15.3.2 + [202] = "Accepted"; -- RFC9110, Section 15.3.3 + [203] = "Non-Authoritative Information"; -- RFC9110, Section 15.3.4 + [204] = "No Content"; -- RFC9110, Section 15.3.5 + [205] = "Reset Content"; -- RFC9110, Section 15.3.6 + [206] = "Partial Content"; -- RFC9110, Section 15.3.7 [207] = "Multi-Status"; [208] = "Already Reported"; -- [209-225] = "Unassigned"; [226] = "IM Used"; -- [227-299] = "Unassigned"; - [300] = "Multiple Choices"; -- RFC7231, Section 6.4.1 - [301] = "Moved Permanently"; -- RFC7231, Section 6.4.2 - [302] = "Found"; -- RFC7231, Section 6.4.3 - [303] = "See Other"; -- RFC7231, Section 6.4.4 - [304] = "Not Modified"; -- RFC7232, Section 4.1 - [305] = "Use Proxy"; -- RFC7231, Section 6.4.5 - -- [306] = "(Unused)"; -- RFC7231, Section 6.4.6 - [307] = "Temporary Redirect"; -- RFC7231, Section 6.4.7 - [308] = "Permanent Redirect"; + [300] = "Multiple Choices"; -- RFC9110, Section 15.4.1 + [301] = "Moved Permanently"; -- RFC9110, Section 15.4.2 + [302] = "Found"; -- RFC9110, Section 15.4.3 + [303] = "See Other"; -- RFC9110, Section 15.4.4 + [304] = "Not Modified"; -- RFC9110, Section 15.4.5 + [305] = "Use Proxy"; -- RFC9110, Section 15.4.6 + -- [306] = "(Unused)"; -- RFC9110, Section 15.4.7 + [307] = "Temporary Redirect"; -- RFC9110, Section 15.4.8 + [308] = "Permanent Redirect"; -- RFC9110, Section 15.4.9 -- [309-399] = "Unassigned"; - [400] = "Bad Request"; -- RFC7231, Section 6.5.1 - [401] = "Unauthorized"; -- RFC7235, Section 3.1 - [402] = "Payment Required"; -- RFC7231, Section 6.5.2 - [403] = "Forbidden"; -- RFC7231, Section 6.5.3 - [404] = "Not Found"; -- RFC7231, Section 6.5.4 - [405] = "Method Not Allowed"; -- RFC7231, Section 6.5.5 - [406] = "Not Acceptable"; -- RFC7231, Section 6.5.6 - [407] = "Proxy Authentication Required"; -- RFC7235, Section 3.2 - [408] = "Request Timeout"; -- RFC7231, Section 6.5.7 - [409] = "Conflict"; -- RFC7231, Section 6.5.8 - [410] = "Gone"; -- RFC7231, Section 6.5.9 - [411] = "Length Required"; -- RFC7231, Section 6.5.10 - [412] = "Precondition Failed"; -- RFC7232, Section 4.2 - [413] = "Payload Too Large"; -- RFC7231, Section 6.5.11 - [414] = "URI Too Long"; -- RFC7231, Section 6.5.12 - [415] = "Unsupported Media Type"; -- RFC7231, Section 6.5.13 - [416] = "Range Not Satisfiable"; -- RFC7233, Section 4.4 - [417] = "Expectation Failed"; -- RFC7231, Section 6.5.14 + [400] = "Bad Request"; -- RFC9110, Section 15.5.1 + [401] = "Unauthorized"; -- RFC9110, Section 15.5.2 + [402] = "Payment Required"; -- RFC9110, Section 15.5.3 + [403] = "Forbidden"; -- RFC9110, Section 15.5.4 + [404] = "Not Found"; -- RFC9110, Section 15.5.5 + [405] = "Method Not Allowed"; -- RFC9110, Section 15.5.6 + [406] = "Not Acceptable"; -- RFC9110, Section 15.5.7 + [407] = "Proxy Authentication Required"; -- RFC9110, Section 15.5.8 + [408] = "Request Timeout"; -- RFC9110, Section 15.5.9 + [409] = "Conflict"; -- RFC9110, Section 15.5.10 + [410] = "Gone"; -- RFC9110, Section 15.5.11 + [411] = "Length Required"; -- RFC9110, Section 15.5.12 + [412] = "Precondition Failed"; -- RFC9110, Section 15.5.13 + [413] = "Content Too Large"; -- RFC9110, Section 15.5.14 + [414] = "URI Too Long"; -- RFC9110, Section 15.5.15 + [415] = "Unsupported Media Type"; -- RFC9110, Section 15.5.16 + [416] = "Range Not Satisfiable"; -- RFC9110, Section 15.5.17 + [417] = "Expectation Failed"; -- RFC9110, Section 15.5.18 [418] = "I'm a teapot"; -- RFC2324, Section 2.3.2 -- [419-420] = "Unassigned"; - [421] = "Misdirected Request"; -- RFC7540, Section 9.1.2 - [422] = "Unprocessable Entity"; + [421] = "Misdirected Request"; -- RFC9110, Section 15.5.20 + [422] = "Unprocessable Content"; -- RFC9110, Section 15.5.21 [423] = "Locked"; [424] = "Failed Dependency"; [425] = "Too Early"; - [426] = "Upgrade Required"; -- RFC7231, Section 6.5.15 + [426] = "Upgrade Required"; -- RFC9110, Section 15.5.22 -- [427] = "Unassigned"; [428] = "Precondition Required"; [429] = "Too Many Requests"; @@ -67,17 +67,17 @@ [451] = "Unavailable For Legal Reasons"; -- [452-499] = "Unassigned"; - [500] = "Internal Server Error"; -- RFC7231, Section 6.6.1 - [501] = "Not Implemented"; -- RFC7231, Section 6.6.2 - [502] = "Bad Gateway"; -- RFC7231, Section 6.6.3 - [503] = "Service Unavailable"; -- RFC7231, Section 6.6.4 - [504] = "Gateway Timeout"; -- RFC7231, Section 6.6.5 - [505] = "HTTP Version Not Supported"; -- RFC7231, Section 6.6.6 + [500] = "Internal Server Error"; -- RFC9110, Section 15.6.1 + [501] = "Not Implemented"; -- RFC9110, Section 15.6.2 + [502] = "Bad Gateway"; -- RFC9110, Section 15.6.3 + [503] = "Service Unavailable"; -- RFC9110, Section 15.6.4 + [504] = "Gateway Timeout"; -- RFC9110, Section 15.6.5 + [505] = "HTTP Version Not Supported"; -- RFC9110, Section 15.6.6 [506] = "Variant Also Negotiates"; [507] = "Insufficient Storage"; [508] = "Loop Detected"; -- [509] = "Unassigned"; - [510] = "Not Extended"; + [510] = "Not Extended"; -- (OBSOLETED) [511] = "Network Authentication Required"; -- [512-599] = "Unassigned"; }; diff -r 54a936345aaa -r 5d9ec2e55d74 net/http/errors.lua --- a/net/http/errors.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/http/errors.lua Wed Mar 27 15:39:03 2024 +0000 @@ -2,8 +2,8 @@ -- and a function to return a util.error object given callback 'code' and 'body' -- parameters. -local codes = require "net.http.codes"; -local util_error = require "util.error"; +local codes = require "prosody.net.http.codes"; +local util_error = require "prosody.util.error"; local error_templates = { -- This code is used by us to report a client-side or connection error. diff -r 54a936345aaa -r 5d9ec2e55d74 net/http/files.lua --- a/net/http/files.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/http/files.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,10 +6,10 @@ -- COPYING file in the source package for more information. -- -local server = require"net.http.server"; +local server = require"prosody.net.http.server"; local lfs = require "lfs"; -local new_cache = require "util.cache".new; -local log = require "util.logger".init("net.http.files"); +local new_cache = require "prosody.util.cache".new; +local log = require "prosody.util.logger".init("net.http.files"); local os_date = os.date; local open = io.open; @@ -23,7 +23,7 @@ forbidden_chars_pattern = "[/%z\001-\031\127\"*:<>?|]" end -local urldecode = require "util.http".urldecode; +local urldecode = require "prosody.util.http".urldecode; local function sanitize_path(path) --> util.paths or util.http? if not path then return end local out = {}; diff -r 54a936345aaa -r 5d9ec2e55d74 net/http/parser.lua --- a/net/http/parser.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/http/parser.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,8 +1,8 @@ local tonumber = tonumber; local assert = assert; local url_parse = require "socket.url".parse; -local urldecode = require "util.http".urldecode; -local dbuffer = require "util.dbuffer"; +local urldecode = require "prosody.util.http".urldecode; +local dbuffer = require "prosody.util.dbuffer"; local function preprocess_path(path) path = urldecode((path:gsub("//+", "/"))); diff -r 54a936345aaa -r 5d9ec2e55d74 net/http/server.lua --- a/net/http/server.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/http/server.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,19 +1,21 @@ local t_insert, t_concat = table.insert, table.concat; -local parser_new = require "net.http.parser".new; -local events = require "util.events".new(); -local addserver = require "net.server".addserver; -local log = require "util.logger".init("http.server"); +local parser_new = require "prosody.net.http.parser".new; +local events = require "prosody.util.events".new(); +local addserver = require "prosody.net.server".addserver; +local logger = require "prosody.util.logger"; +local log = logger.init("http.server"); local os_date = os.date; local pairs = pairs; local s_upper = string.upper; local setmetatable = setmetatable; -local cache = require "util.cache"; -local codes = require "net.http.codes"; -local promise = require "util.promise"; -local errors = require "util.error"; +local cache = require "prosody.util.cache"; +local codes = require "prosody.net.http.codes"; +local promise = require "prosody.util.promise"; +local errors = require "prosody.util.error"; local blocksize = 2^16; -local async = require "util.async"; +local async = require "prosody.util.async"; +local id = require"prosody.util.id"; local _M = {}; @@ -105,7 +107,12 @@ function runner_callbacks:error(err) log("error", "Traceback[httpserver]: %s", err); - self.data.conn:write("HTTP/1.0 500 Internal Server Error\r\n\r\n"..events.fire_event("http-error", { code = 500, private_message = err })); + local response = { headers = { content_type = "text/plain" }; body = "" }; + response.body = events.fire_event("http-error", { code = 500; private_message = err; response = response }); + self.data.conn:write("HTTP/1.0 500 Internal Server Error\r\n\z + X-Content-Type-Options: nosniff\r\n\z + Content-Type: " .. response.headers.content_type .. "\r\n\r\n"); + self.data.conn:write(response.body); self.data.conn:close(); end @@ -128,6 +135,8 @@ end, runner_callbacks, session); local function success_cb(request) --log("debug", "success_cb: %s", request.path); + request.id = id.short(); + request.log = logger.init("http." .. request.method .. "-" .. request.id); request.ip = ip; request.secure = secure; session.thread:run(request); @@ -232,6 +241,8 @@ request.headers = headers; request.conn = conn; + request.log("debug", "%s %s HTTP/%s", request.method, request.path, request.httpversion); + local date_header = os_date('!%a, %d %b %Y %H:%M:%S GMT'); -- FIXME use local conn_header = request.headers.connection; conn_header = conn_header and ","..conn_header:gsub("[ \t]", ""):lower().."," or "" @@ -249,10 +260,12 @@ local is_head_request = request.method == "HEAD"; local response = { + id = request.id; + log = request.log; request = request; is_head_request = is_head_request; status_code = 200; - headers = { date = date_header, connection = response_conn_header }; + headers = { date = date_header; connection = response_conn_header; x_request_id = request.id }; persistent = persistent; conn = conn; send = _M.send_response; @@ -281,11 +294,9 @@ local global_event = request.method.." "..request.path:match("[^?]*"); local payload = { request = request, response = response }; - log("debug", "Firing event: %s", global_event); local result = events.fire_event(global_event, payload); if result == nil and is_head_request then local global_head_event = "GET "..request.path:match("[^?]*"); - log("debug", "Firing event: %s", global_head_event); result = events.fire_event(global_head_event, payload); end if result == nil then @@ -306,12 +317,10 @@ end local host_event = request.method.." "..host..request.path:match("[^?]*"); - log("debug", "Firing event: %s", host_event); result = events.fire_event(host_event, payload); if result == nil and is_head_request then local host_head_event = "GET "..host..request.path:match("[^?]*"); - log("debug", "Firing event: %s", host_head_event); result = events.fire_event(host_head_event, payload); end end @@ -321,6 +330,7 @@ local function prepare_header(response) local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]); + response.log("debug", "%s", status_line); local headers = response.headers; local output = { status_line }; for k,v in pairs(headers) do @@ -378,11 +388,11 @@ response.conn:write(chunk); else incomplete[response.conn] = nil; + if f.close then f:close(); end if chunked then response.conn:write("0\r\n\r\n"); end -- io.write("\n"); - if f.close then f:close(); end return response:done(); end end @@ -397,9 +407,8 @@ response:on_destroy(); response.on_destroy = nil; end - if response.persistent then - response:finish_cb(); - else + response:finish_cb(); + if not response.persistent then response.conn:close(); end end diff -r 54a936345aaa -r 5d9ec2e55d74 net/httpserver.lua --- a/net/httpserver.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/httpserver.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,5 +1,5 @@ -- COMPAT w/pre-0.9 -local log = require "util.logger".init("net.httpserver"); +local log = require "prosody.util.logger".init("net.httpserver"); local traceback = debug.traceback; local _ENV = nil; diff -r 54a936345aaa -r 5d9ec2e55d74 net/resolvers/basic.lua --- a/net/resolvers/basic.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/resolvers/basic.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,14 +1,62 @@ -local adns = require "net.adns"; -local inet_pton = require "util.net".pton; -local inet_ntop = require "util.net".ntop; -local idna_to_ascii = require "util.encodings".idna.to_ascii; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local adns = require "prosody.net.adns"; +local inet_pton = require "prosody.util.net".pton; +local inet_ntop = require "prosody.util.net".ntop; +local idna_to_ascii = require "prosody.util.encodings".idna.to_ascii; +local promise = require "prosody.util.promise"; +local t_move = require "prosody.util.table".move; local methods = {}; local resolver_mt = { __index = methods }; -- FIXME RFC 6724 +local function do_dns_lookup(self, dns_resolver, record_type, name, allow_insecure) + return promise.new(function (resolve, reject) + local ipv = (record_type == "A" and "4") or (record_type == "AAAA" and "6") or nil; + if ipv and self.extra["use_ipv"..ipv] == false then + return reject(("IPv%s disabled - %s lookup skipped"):format(ipv, record_type)); + elseif record_type == "TLSA" and self.extra.use_dane ~= true then + return reject("DANE disabled - TLSA lookup skipped"); + end + dns_resolver:lookup(function (answer, err) + if not answer then + return reject(err); + elseif answer.bogus then + return reject(("Validation error in %s lookup"):format(record_type)); + elseif not (answer.secure or allow_insecure) then + return reject(("Insecure response in %s lookup"):format(record_type)); + elseif answer.status and #answer == 0 then + return reject(("%s in %s lookup"):format(answer.status, record_type)); + end + + local targets = { secure = answer.secure }; + for _, record in ipairs(answer) do + if ipv then + table.insert(targets, { self.conn_type..ipv, record[record_type:lower()], self.port, self.extra }); + else + table.insert(targets, record[record_type:lower()]); + end + end + return resolve(targets); + end, name, record_type, "IN"); + end); +end + +local function merge_targets(ipv4_targets, ipv6_targets) + local result = { secure = ipv4_targets.secure and ipv6_targets.secure }; + local common_length = math.min(#ipv4_targets, #ipv6_targets); + for i = 1, common_length do + table.insert(result, ipv6_targets[i]); + table.insert(result, ipv4_targets[i]); + end + if common_length < #ipv4_targets then + t_move(ipv4_targets, common_length+1, #ipv4_targets, common_length+1, result); + elseif common_length < #ipv6_targets then + t_move(ipv6_targets, common_length+1, #ipv6_targets, common_length+1, result); + end + return result; +end + -- Find the next target to connect to, and -- pass it to cb() function methods:next(cb) @@ -18,7 +66,7 @@ return; end local next_target = table.remove(self.targets, 1); - cb(unpack(next_target, 1, 4)); + cb(next_target[1], next_target[2], next_target[3], next_target[4], not not self.targets[1]); return; end @@ -28,91 +76,47 @@ return; end - local secure = true; - local tlsa = {}; - local targets = {}; - local n = 3; - local function ready() - n = n - 1; - if n > 0 then return; end - self.targets = targets; + -- Resolve DNS to target list + local dns_resolver = adns.resolver(); + + local dns_lookups = { + ipv4 = do_dns_lookup(self, dns_resolver, "A", self.hostname, true); + ipv6 = do_dns_lookup(self, dns_resolver, "AAAA", self.hostname, true); + tlsa = do_dns_lookup(self, dns_resolver, "TLSA", ("_%d._%s.%s"):format(self.port, self.conn_type, self.hostname)); + }; + + promise.all_settled(dns_lookups):next(function (dns_results) + -- Combine targets, assign to self.targets, self:next(cb) + local have_ipv4 = dns_results.ipv4.status == "fulfilled"; + local have_ipv6 = dns_results.ipv6.status == "fulfilled"; + + if have_ipv4 and have_ipv6 then + self.targets = merge_targets(dns_results.ipv4.value, dns_results.ipv6.value); + elseif have_ipv4 then + self.targets = dns_results.ipv4.value; + elseif have_ipv6 then + self.targets = dns_results.ipv6.value; + else + self.targets = {}; + end + if self.extra and self.extra.use_dane then - if secure and tlsa[1] then - self.extra.tlsa = tlsa; + if self.targets.secure and dns_results.tlsa.status == "fulfilled" then + self.extra.tlsa = dns_results.tlsa.value; self.extra.dane_hostname = self.hostname; else self.extra.tlsa = nil; self.extra.dane_hostname = nil; end + elseif self.extra and self.extra.srv_secure then + self.extra.secure_hostname = self.hostname; end - self:next(cb); - end - - -- Resolve DNS to target list - local dns_resolver = adns.resolver(); - - if not self.extra or self.extra.use_ipv4 ~= false then - dns_resolver:lookup(function (answer, err) - if answer then - secure = secure and answer.secure; - for _, record in ipairs(answer) do - table.insert(targets, { self.conn_type.."4", record.a, self.port, self.extra }); - end - if answer.bogus then - self.last_error = "Validation error in A lookup"; - elseif answer.status then - self.last_error = answer.status .. " in A lookup"; - end - else - self.last_error = err; - end - ready(); - end, self.hostname, "A", "IN"); - else - ready(); - end - if not self.extra or self.extra.use_ipv6 ~= false then - dns_resolver:lookup(function (answer, err) - if answer then - secure = secure and answer.secure; - for _, record in ipairs(answer) do - table.insert(targets, { self.conn_type.."6", record.aaaa, self.port, self.extra }); - end - if answer.bogus then - self.last_error = "Validation error in AAAA lookup"; - elseif answer.status then - self.last_error = answer.status .. " in AAAA lookup"; - end - else - self.last_error = err; - end - ready(); - end, self.hostname, "AAAA", "IN"); - else - ready(); - end - - if self.extra and self.extra.use_dane == true then - dns_resolver:lookup(function (answer, err) - if answer then - secure = secure and answer.secure; - for _, record in ipairs(answer) do - table.insert(tlsa, record.tlsa); - end - if answer.bogus then - self.last_error = "Validation error in TLSA lookup"; - elseif answer.status then - self.last_error = answer.status .. " in TLSA lookup"; - end - else - self.last_error = err; - end - ready(); - end, ("_%d._tcp.%s"):format(self.port, self.hostname), "TLSA", "IN"); - else - ready(); - end + self:next(cb); + end):catch(function (err) + self.last_error = err; + self.targets = {}; + end); end local function new(hostname, port, conn_type, extra) @@ -137,7 +141,7 @@ hostname = ascii_host; port = port; conn_type = conn_type; - extra = extra; + extra = extra or {}; targets = targets; }, resolver_mt); end diff -r 54a936345aaa -r 5d9ec2e55d74 net/resolvers/manual.lua --- a/net/resolvers/manual.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/resolvers/manual.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,6 @@ local methods = {}; local resolver_mt = { __index = methods }; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; -- Find the next target to connect to, and -- pass it to cb() diff -r 54a936345aaa -r 5d9ec2e55d74 net/resolvers/service.lua --- a/net/resolvers/service.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/resolvers/service.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,24 +1,79 @@ -local adns = require "net.adns"; -local basic = require "net.resolvers.basic"; -local inet_pton = require "util.net".pton; -local idna_to_ascii = require "util.encodings".idna.to_ascii; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local adns = require "prosody.net.adns"; +local basic = require "prosody.net.resolvers.basic"; +local inet_pton = require "prosody.util.net".pton; +local idna_to_ascii = require "prosody.util.encodings".idna.to_ascii; local methods = {}; local resolver_mt = { __index = methods }; +local function new_target_selector(rrset) + local rr_count = rrset and #rrset; + if not rr_count or rr_count == 0 then + rrset = nil; + else + table.sort(rrset, function (a, b) return a.srv.priority < b.srv.priority end); + end + local rrset_pos = 1; + local priority_bucket, bucket_total_weight, bucket_len, bucket_used; + return function () + if not rrset then return; end + + if not priority_bucket or bucket_used >= bucket_len then + if rrset_pos > rr_count then return; end -- Used up all records + + -- Going to start on a new priority now. Gather up all the next + -- records with the same priority and add them to priority_bucket + priority_bucket, bucket_total_weight, bucket_len, bucket_used = {}, 0, 0, 0; + local current_priority; + repeat + local curr_record = rrset[rrset_pos].srv; + if not current_priority then + current_priority = curr_record.priority; + elseif current_priority ~= curr_record.priority then + break; + end + table.insert(priority_bucket, curr_record); + bucket_total_weight = bucket_total_weight + curr_record.weight; + bucket_len = bucket_len + 1; + rrset_pos = rrset_pos + 1; + until rrset_pos > rr_count; + end + + bucket_used = bucket_used + 1; + local n, running_total = math.random(0, bucket_total_weight), 0; + local target_record; + for i = 1, bucket_len do + local candidate = priority_bucket[i]; + if candidate then + running_total = running_total + candidate.weight; + if running_total >= n then + target_record = candidate; + bucket_total_weight = bucket_total_weight - candidate.weight; + priority_bucket[i] = nil; + break; + end + end + end + return target_record; + end; +end + -- Find the next target to connect to, and -- pass it to cb() function methods:next(cb) - if self.targets then - if not self.resolver then - if #self.targets == 0 then + if self.resolver or self._get_next_target then + if not self.resolver then -- Do we have a basic resolver currently? + -- We don't, so fetch a new SRV target, create a new basic resolver for it + local next_srv_target = self._get_next_target and self._get_next_target(); + if not next_srv_target then + -- No more SRV targets left cb(nil); return; end - local next_target = table.remove(self.targets, 1); - self.resolver = basic.new(unpack(next_target, 1, 4)); + -- Create a new basic resolver for this SRV target + self.resolver = basic.new(next_srv_target.target, next_srv_target.port, self.conn_type, self.extra); end + -- Look up the next (basic) target from the current target's resolver self.resolver:next(function (...) if self.resolver then self.last_error = self.resolver.last_error; @@ -31,6 +86,9 @@ end end); return; + elseif self.in_progress then + cb(nil); + return; end if not self.hostname then @@ -39,9 +97,9 @@ return; end - local targets = {}; + self.in_progress = true; + local function ready() - self.targets = targets; self:next(cb); end @@ -53,17 +111,23 @@ answer = {}; end if answer then - if self.extra and not answer.secure then - self.extra.use_dane = false; - elseif answer.bogus then + if answer.bogus then self.last_error = "Validation error in SRV lookup"; ready(); return; + elseif not answer.secure then + if self.extra then + -- Insecure results, so no DANE + self.extra.use_dane = false; + end + end + if self.extra then + self.extra.srv_secure = answer.secure; end if #answer == 0 then if self.extra and self.extra.default_port then - table.insert(targets, { self.hostname, self.extra.default_port, self.conn_type, self.extra }); + self.resolver = basic.new(self.hostname, self.extra.default_port, self.conn_type, self.extra); else self.last_error = "zero SRV records found"; end @@ -77,10 +141,7 @@ return; end - table.sort(answer, function (a, b) return a.srv.priority < b.srv.priority end); - for _, record in ipairs(answer) do - table.insert(targets, { record.srv.target, record.srv.port, self.conn_type, self.extra }); - end + self._get_next_target = new_target_selector(answer); else self.last_error = err; end diff -r 54a936345aaa -r 5d9ec2e55d74 net/server.lua --- a/net/server.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/server.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,20 +6,23 @@ -- COPYING file in the source package for more information. -- -if not (prosody and prosody.config_loaded) then - -- This module only supports loading inside Prosody, outside Prosody - -- you should directly require net.server_select or server_event, etc. - error(debug.traceback("Loading outside Prosody or Prosody not yet initialized"), 0); +local function log(level, format, ...) + print("net.server", level, format:format(...)); end -local log = require "util.logger".init("net.server"); - -local default_backend = "epoll"; +local default_backend = "select"; +local server_type = default_backend; -local server_type = require "core.configmanager".get("*", "network_backend") or default_backend; +if (prosody and prosody.config_loaded) then + default_backend = "epoll"; + log = require"prosody.util.logger".init("net.server"); + server_type = require"prosody.core.configmanager".get("*", "network_backend") or default_backend; -if require "core.configmanager".get("*", "use_libevent") then - server_type = "event"; + if require"prosody.core.configmanager".get("*", "use_libevent") then + server_type = "event"; + end +elseif pcall(require, "prosody.util.poll") then + server_type = "epoll"; end if server_type == "event" then @@ -32,7 +35,7 @@ local server; local set_config; if server_type == "event" then - server = require "net.server_event"; + server = require "prosody.net.server_event"; local defaults = {}; for k,v in pairs(server.cfg) do @@ -61,7 +64,7 @@ elseif server_type == "select" then -- TODO Remove completely. log("warn", "select is deprecated, the new default is epoll. For more info see https://prosody.im/doc/network_backend"); - server = require "net.server_select"; + server = require "prosody.net.server_select"; local defaults = {}; for k,v in pairs(server.getsettings()) do @@ -75,7 +78,7 @@ server.changesettings(select_settings); end else - server = require("net.server_"..server_type); + server = require("prosody.net.server_"..server_type); set_config = server.set_config; if not server.get_backend then function server.get_backend() @@ -85,7 +88,7 @@ end -- If server.hook_signal exists, replace signal.signal() -local has_signal, signal = pcall(require, "util.signal"); +local has_signal, signal = pcall(require, "prosody.util.signal"); if has_signal then if server.hook_signal then function signal.signal(signal_id, handler) @@ -109,7 +112,7 @@ end if prosody and set_config then - local config_get = require "core.configmanager".get; + local config_get = require "prosody.core.configmanager".get; local function load_config() local settings = config_get("*", "network_settings") or {}; return set_config(settings); @@ -118,6 +121,15 @@ prosody.events.add_handler("config-reloaded", load_config); end --- require "net.server" shall now forever return this, +if prosody and server.tls_builder then + local tls_builder = server.tls_builder; + -- resolving the basedir here avoids util.sslconfig depending on + -- prosody.paths.config + function server.tls_builder() + return tls_builder(prosody.paths.config or "") + end +end + +-- require "prosody.net.server" shall now forever return this, -- ie. server_select or server_event as chosen above. return server; diff -r 54a936345aaa -r 5d9ec2e55d74 net/server_epoll.lua --- a/net/server_epoll.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/server_epoll.lua Wed Mar 27 15:39:03 2024 +0000 @@ -15,21 +15,23 @@ local pairs = pairs; local ipairs = ipairs; local traceback = debug.traceback; -local logger = require "util.logger"; +local logger = require "prosody.util.logger"; local log = logger.init("server_epoll"); local socket = require "socket"; -local luasec = require "ssl"; -local realtime = require "util.time".now; -local monotonic = require "util.time".monotonic; -local indexedbheap = require "util.indexedbheap"; -local createtable = require "util.table".create; -local inet = require "util.net"; +local realtime = require "prosody.util.time".now; +local monotonic = require "prosody.util.time".monotonic; +local indexedbheap = require "prosody.util.indexedbheap"; +local createtable = require "prosody.util.table".create; +local inet = require "prosody.util.net"; local inet_pton = inet.pton; local _SOCKETINVALID = socket._SOCKETINVALID or -1; -local new_id = require "util.id".short; -local xpcall = require "util.xpcall".xpcall; +local new_id = require "prosody.util.id".short; +local xpcall = require "prosody.util.xpcall".xpcall; +local sslconfig = require "prosody.util.sslconfig"; +local tls_impl = require "prosody.net.tls_luasec"; +local have_signal, signal = pcall(require, "prosody.util.signal"); -local poller = require "util.poll" +local poller = require "prosody.util.poll" local EEXIST = poller.EEXIST; local ENOENT = poller.ENOENT; @@ -91,6 +93,12 @@ --- How long to wait after getting the shutdown signal before forcefully tearing down every socket shutdown_deadline = 5; + + -- TCP Fast Open + tcp_fastopen = false; + + -- Defer accept until incoming data is available + tcp_defer_accept = false; }}; local cfg = default_config.__index; @@ -393,6 +401,9 @@ end if r == nil then r = self._wantread; end if w == nil then w = self._wantwrite; end + if r == self._wantread and w == self._wantwrite then + return true + end local ok, err, errno = poll:set(fd, r, w); if not ok then self:debug("Could not update poller state: %s(%d)", err, errno); @@ -614,6 +625,47 @@ self._sslctx = sslctx; end +function interface:sslctx() + return self.tls_ctx +end + +function interface:ssl_info() + local sock = self.conn; + if not sock then return nil, "not-connected" end + if not sock.info then return nil, "not-implemented"; end + return sock:info(); +end + +function interface:ssl_peercertificate() + local sock = self.conn; + if not sock then return nil, "not-connected" end + if not sock.getpeercertificate then return nil, "not-implemented"; end + return sock:getpeercertificate(); +end + +function interface:ssl_peerverification() + local sock = self.conn; + if not sock then return nil, "not-connected" end + if not sock.getpeerverification then return nil, { { "Chain verification not supported" } }; end + return sock:getpeerverification(); +end + +function interface:ssl_peerfinished() + local sock = self.conn; + if not sock then return nil, "not-connected" end + if not sock.getpeerfinished then return nil, "not-implemented"; end + return sock:getpeerfinished(); +end + +function interface:ssl_exportkeyingmaterial(label, len, context) + local sock = self.conn; + if not sock then return nil, "not-connected" end + if sock.exportkeyingmaterial then + return sock:exportkeyingmaterial(label, len, context); + end +end + + function interface:starttls(tls_ctx) if tls_ctx then self.tls_ctx = tls_ctx; end self.starttls = false; @@ -641,11 +693,7 @@ self.starttls = false; self:debug("Starting TLS now"); self:updatenames(); -- Can't getpeer/sockname after wrap() - local ok, conn, err = pcall(luasec.wrap, self.conn, self.tls_ctx); - if not ok then - conn, err = ok, conn; - self:debug("Failed to initialize TLS: %s", err); - end + local conn, err = self.tls_ctx:wrap(self.conn); if not conn then self:on("disconnect", err); self:destroy(); @@ -656,8 +704,8 @@ if conn.sni then if self.servername then conn:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - conn:sni(self._server.hosts, true); + elseif next(self.tls_ctx._sni_contexts) ~= nil then + conn:sni(self.tls_ctx._sni_contexts, true); end end if self.extra and self.extra.tlsa and conn.settlsa then @@ -741,7 +789,6 @@ end end - conn:updatenames(); return conn; end @@ -767,6 +814,7 @@ return; end local client = wrapsocket(conn, self, nil, self.listeners); + client:updatenames(); client:debug("New connection %s on server %s", client, self); client:defaultoptions(); client._writable = cfg.opportunistic_writes; @@ -885,6 +933,12 @@ log = logger.init(("serv%s"):format(new_id())); }, interface_mt); server:debug("Server %s created", server); + if cfg.tcp_fastopen then + server:setoption("tcp-fastopen", cfg.tcp_fastopen); + end + if type(cfg.tcp_defer_accept) == "number" then + server:setoption("tcp-defer-accept", cfg.tcp_defer_accept); + end server:add(true, false); return server; end @@ -908,6 +962,7 @@ -- COMPAT local function wrapclient(conn, addr, port, listeners, read_size, tls_ctx, extra) local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra); + client:updatenames(); if not client.peername then client.peername, client.peerport = addr, port; end @@ -941,9 +996,13 @@ if not conn then return conn, err; end local ok, err = conn:settimeout(0); if not ok then return ok, err; end + local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra) + if cfg.tcp_fastopen then + client:setoption("tcp-fastopen-connect", 1); + end local ok, err = conn:setpeername(addr, port); if not ok and err ~= "timeout" then return ok, err; end - local client = wrapsocket(conn, nil, read_size, listeners, tls_ctx, extra) + client:updatenames(); local ok, err = client:init(); if not client.peername then -- otherwise not set until connected @@ -1032,12 +1091,38 @@ end end +local function loop_once() + runtimers(); -- Ignore return value because we only do this once + local fd, r, w = poll:wait(0); + if fd then + local conn = fds[fd]; + if conn then + if r then + conn:onreadable(); + end + if w then + conn:onwritable(); + end + else + log("debug", "Removing unknown fd %d", fd); + poll:del(fd); + end + else + return fd, r; + end +end + -- Main loop local function loop(once) - repeat - local t = runtimers(cfg.max_wait, cfg.min_wait); + if once then + return loop_once(); + end + + local t = 0; + while not quitting do local fd, r, w = poll:wait(t); - while fd do + if fd then + t = 0; local conn = fds[fd]; if conn then if r then @@ -1050,15 +1135,35 @@ log("debug", "Removing unknown fd %d", fd); poll:del(fd); end - fd, r, w = poll:wait(0); - end - if r ~= "timeout" and r ~= "signal" then + elseif r == "timeout" then + t = runtimers(cfg.max_wait, cfg.min_wait); + elseif r ~= "signal" then log("debug", "epoll_wait error: %s[%d]", r, w); end - until once or (quitting and next(fds) == nil); + end return quitting; end +local hook_signal; +if have_signal and signal.signalfd then + local function dispatch(self) + return self:on("signal", self.conn:read()); + end + + function hook_signal(signum, cb) + local sigfd = signal.signalfd(signum); + if not sigfd then + log("error", "Could not hook signal %d", signum); + return nil, "failed"; + end + local watch = watchfd(sigfd, dispatch); + watch.listeners = { onsignal = cb }; + watch.close = nil; -- revert to default + watch:noise("Signal handler %d ready", signum); + return watch; + end +end + return { get_backend = function () return "epoll"; end; addserver = addserver; @@ -1084,6 +1189,11 @@ set_config = function (newconfig) cfg = setmetatable(newconfig, default_config); end; + hook_signal = hook_signal; + + tls_builder = function(basedir) + return sslconfig._new(tls_impl.new_context, basedir) + end, -- libevent emulation event = { EV_READ = "r", EV_WRITE = "w", EV_READWRITE = "rw", EV_LEAVE = -1 }; diff -r 54a936345aaa -r 5d9ec2e55d74 net/server_event.lua --- a/net/server_event.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/server_event.lua Wed Mar 27 15:39:03 2024 +0000 @@ -47,15 +47,17 @@ local coroutine_wrap = coroutine.wrap local coroutine_yield = coroutine.yield -local has_luasec, ssl = pcall ( require , "ssl" ) +local has_luasec = pcall ( require , "ssl" ) local socket = require "socket" local levent = require "luaevent.core" -local inet = require "util.net"; +local inet = require "prosody.util.net"; local inet_pton = inet.pton; +local sslconfig = require "prosody.util.sslconfig"; +local tls_impl = require "prosody.net.tls_luasec"; local socket_gettime = socket.gettime -local log = require ("util.logger").init("socket") +local log = require ("prosody.util.logger").init("socket") local function debug(...) return log("debug", ("%s "):rep(select('#', ...)), ...) @@ -153,7 +155,7 @@ _ = self.eventwrite and self.eventwrite:close( ) self.eventread, self.eventwrite = nil, nil local err - self.conn, err = ssl.wrap( self.conn, self._sslctx ) + self.conn, err = self._sslctx:wrap(self.conn) if err then self.fatalerror = err self.conn = nil -- cannot be used anymore @@ -168,8 +170,8 @@ if self.conn.sni then if self.servername then self.conn:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - self.conn:sni(self._server.hosts, true); + elseif next(self._sslctx._sni_contexts) ~= nil then + self.conn:sni(self._sslctx._sni_contexts, true); end end @@ -274,6 +276,34 @@ return self:_lock(self.nointerface, true, self.nowriting); end +function interface_mt:sslctx() + return self._sslctx +end + +function interface_mt:ssl_info() + local sock = self.conn; + if not sock.info then return nil, "not-implemented"; end + return sock:info(); +end + +function interface_mt:ssl_peercertificate() + local sock = self.conn; + if not sock.getpeercertificate then return nil, "not-implemented"; end + return sock:getpeercertificate(); +end + +function interface_mt:ssl_peerverification() + local sock = self.conn; + if not sock.getpeerverification then return nil, { { "Chain verification not supported" } }; end + return sock:getpeerverification(); +end + +function interface_mt:ssl_peerfinished() + local sock = self.conn; + if not sock.getpeerfinished then return nil, "not-implemented"; end + return sock:getpeerfinished(); +end + function interface_mt:resume() self:_lock(self.nointerface, false, self.nowriting); if self.readcallback and not self.eventread then @@ -924,6 +954,10 @@ add_task = add_task, watchfd = watchfd, + tls_builder = function(basedir) + return sslconfig._new(tls_impl.new_context, basedir) + end, + __NAME = SCRIPT_NAME, __DATE = LAST_MODIFIED, __AUTHOR = SCRIPT_AUTHOR, diff -r 54a936345aaa -r 5d9ec2e55d74 net/server_select.lua --- a/net/server_select.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/server_select.lua Wed Mar 27 15:39:03 2024 +0000 @@ -11,7 +11,7 @@ return _G[ what ] end -local log, table_concat = require ("util.logger").init("socket"), table.concat; +local log, table_concat = require ("prosody.util.logger").init("socket"), table.concat; local out_put = function (...) return log("debug", table_concat{...}); end local out_error = function (...) return log("warn", table_concat{...}); end @@ -47,15 +47,15 @@ --// extern libs //-- -local has_luasec, luasec = pcall ( require , "ssl" ) local luasocket = use "socket" or require "socket" local luasocket_gettime = luasocket.gettime -local inet = require "util.net"; +local inet = require "prosody.util.net"; local inet_pton = inet.pton; +local sslconfig = require "prosody.util.sslconfig"; +local has_luasec, tls_impl = pcall(require, "prosody.net.tls_luasec"); --// extern lib methods //-- -local ssl_wrap = ( has_luasec and luasec.wrap ) local socket_bind = luasocket.bind local socket_select = luasocket.select @@ -359,6 +359,21 @@ handler.sslctx = function ( ) return sslctx end + handler.ssl_info = function( ) + return socket.info and socket:info() + end + handler.ssl_peercertificate = function( ) + if not socket.getpeercertificate then return nil, "not-implemented"; end + return socket:getpeercertificate() + end + handler.ssl_peerverification = function( ) + if not socket.getpeerverification then return nil, { { "Chain verification not supported" } }; end + return socket:getpeerverification(); + end + handler.ssl_peerfinished = function( ) + if not socket.getpeerfinished then return nil, "not-implemented"; end + return socket:getpeerfinished(); + end handler.send = function( _, data, i, j ) return send( socket, data, i, j ) end @@ -652,7 +667,7 @@ end out_put( "server.lua: attempting to start tls on " .. tostring( socket ) ) local oldsocket, err = socket - socket, err = ssl_wrap( socket, sslctx ) -- wrap socket + socket, err = sslctx:wrap(socket) -- wrap socket if not socket then out_put( "server.lua: error while starting tls on client: ", tostring(err or "unknown error") ) @@ -662,8 +677,8 @@ if socket.sni then if self.servername then socket:sni(self.servername); - elseif self._server and type(self._server.hosts) == "table" and next(self._server.hosts) ~= nil then - socket:sni(self.server().hosts, true); + elseif next(sslctx._sni_contexts) ~= nil then + socket:sni(sslctx._sni_contexts, true); end end @@ -1169,4 +1184,8 @@ removeserver = removeserver, get_backend = get_backend, changesettings = changesettings, + + tls_builder = function(basedir) + return sslconfig._new(tls_impl.new_context, basedir) + end, } diff -r 54a936345aaa -r 5d9ec2e55d74 net/stun.lua --- a/net/stun.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/stun.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,11 +1,11 @@ -local base64 = require "util.encodings".base64; -local hashes = require "util.hashes"; -local net = require "util.net"; -local random = require "util.random"; -local struct = require "util.struct"; -local bit32 = require"util.bitcompat"; -local sxor = require"util.strbitop".sxor; -local new_ip = require "util.ip".new_ip; +local base64 = require "prosody.util.encodings".base64; +local hashes = require "prosody.util.hashes"; +local net = require "prosody.util.net"; +local random = require "prosody.util.random"; +local struct = require "prosody.util.struct"; +local bit32 = require"prosody.util.bitcompat"; +local sxor = require"prosody.util.strbitop".sxor; +local new_ip = require "prosody.util.ip".new_ip; --- Public helpers diff -r 54a936345aaa -r 5d9ec2e55d74 net/tls_luasec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/net/tls_luasec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,114 @@ +-- Prosody IM +-- Copyright (C) 2021 Prosody folks +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +--[[ +This file provides a shim abstraction over LuaSec, consolidating some code +which was previously spread between net.server backends, portmanager and +certmanager. + +The goal is to provide a more or less well-defined API on top of LuaSec which +abstracts away some of the things which are not needed and simplifies usage of +commonly used things (such as SNI contexts). Eventually, network backends +which do not rely on LuaSocket+LuaSec should be able to provide *this* API +instead of having to mimic LuaSec. +]] +local ssl = require "ssl"; +local ssl_newcontext = ssl.newcontext; +local ssl_context = ssl.context or require "ssl.context"; +local io_open = io.open; + +local context_api = {}; +local context_mt = {__index = context_api}; + +function context_api:set_sni_host(host, cert, key) + local ctx, err = self._builder:clone():apply({ + certificate = cert, + key = key, + }):build(); + if not ctx then + return false, err + end + + self._sni_contexts[host] = ctx._inner + + return true, nil +end + +function context_api:remove_sni_host(host) + self._sni_contexts[host] = nil +end + +function context_api:wrap(sock) + local ok, conn, err = pcall(ssl.wrap, sock, self._inner); + if not ok then + return nil, err + end + return conn, nil +end + +local function new_context(cfg, builder) + -- LuaSec expects dhparam to be a callback that takes two arguments. + -- We ignore those because it is mostly used for having a separate + -- set of params for EXPORT ciphers, which we don't have by default. + if type(cfg.dhparam) == "string" then + local f, err = io_open(cfg.dhparam); + if not f then return nil, "Could not open DH parameters: "..err end + local dhparam = f:read("*a"); + f:close(); + cfg.dhparam = function() return dhparam; end + end + + local inner, err = ssl_newcontext(cfg); + if not inner then + return nil, err + end + + -- COMPAT Older LuaSec ignores the cipher list from the config, so we have to take care + -- of it ourselves (W/A for #x) + if inner and cfg.ciphers then + local success; + success, err = ssl_context.setcipher(inner, cfg.ciphers); + if not success then + return nil, err + end + end + + return setmetatable({ + _inner = inner, + _builder = builder, + _sni_contexts = {}, + }, context_mt), nil +end + +-- Feature detection / guessing +local function test_option(option) + return not not ssl_newcontext({mode="server",protocol="sslv23",options={ option }}); +end +local luasec_major, luasec_minor = ssl._VERSION:match("^(%d+)%.(%d+)"); +local luasec_version = tonumber(luasec_major) * 100 + tonumber(luasec_minor); +local luasec_has = ssl.config or { + algorithms = { + ec = luasec_version >= 5; + }; + capabilities = { + curves_list = luasec_version >= 7; + }; + options = { + cipher_server_preference = test_option("cipher_server_preference"); + no_ticket = test_option("no_ticket"); + no_compression = test_option("no_compression"); + single_dh_use = test_option("single_dh_use"); + single_ecdh_use = test_option("single_ecdh_use"); + no_renegotiation = test_option("no_renegotiation"); + }; +}; + +return { + features = luasec_has; + new_context = new_context, + load_certificate = ssl.loadcertificate; +}; diff -r 54a936345aaa -r 5d9ec2e55d74 net/unbound.lua --- a/net/unbound.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/unbound.lua Wed Mar 27 15:39:03 2024 +0000 @@ -13,15 +13,15 @@ local s_upper = string.upper; local noop = function() end; -local logger = require "util.logger"; +local logger = require "prosody.util.logger"; local log = logger.init("unbound"); -local net_server = require "net.server"; +local net_server = require "prosody.net.server"; local libunbound = require"lunbound"; -local promise = require"util.promise"; -local new_id = require "util.id".short; +local promise = require"prosody.util.promise"; +local new_id = require "prosody.util.id".short; local gettime = require"socket".gettime; -local dns_utils = require"util.dns"; +local dns_utils = require"prosody.util.dns"; local classes, types, errors = dns_utils.classes, dns_utils.types, dns_utils.errors; local parsers = dns_utils.parsers; @@ -44,7 +44,7 @@ local unbound_config; if prosody then - local config = require"core.configmanager"; + local config = require"prosody.core.configmanager"; unbound_config = add_defaults(config.get("*", "unbound")); prosody.events.add_handler("config-reloaded", function() unbound_config = add_defaults(config.get("*", "unbound")); diff -r 54a936345aaa -r 5d9ec2e55d74 net/websocket.lua --- a/net/websocket.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/websocket.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,13 +8,13 @@ local t_concat = table.concat; -local http = require "net.http"; -local frames = require "net.websocket.frames"; -local base64 = require "util.encodings".base64; -local sha1 = require "util.hashes".sha1; -local random_bytes = require "util.random".bytes; -local timer = require "util.timer"; -local log = require "util.logger".init "websocket"; +local http = require "prosody.net.http"; +local frames = require "prosody.net.websocket.frames"; +local base64 = require "prosody.util.encodings".base64; +local sha1 = require "prosody.util.hashes".sha1; +local random_bytes = require "prosody.util.random".bytes; +local timer = require "prosody.util.timer"; +local log = require "prosody.util.logger".init "websocket"; local close_timeout = 3; -- Seconds to wait after sending close frame until closing connection. diff -r 54a936345aaa -r 5d9ec2e55d74 net/websocket/frames.lua --- a/net/websocket/frames.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/net/websocket/frames.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,17 +6,17 @@ -- COPYING file in the source package for more information. -- -local random_bytes = require "util.random".bytes; +local random_bytes = require "prosody.util.random".bytes; -local bit = require "util.bitcompat"; +local bit = require "prosody.util.bitcompat"; local band = bit.band; local bor = bit.bor; -local sbit = require "util.strbitop"; +local sbit = require "prosody.util.strbitop"; local sxor = sbit.sxor; local s_char = string.char; -local s_pack = require"util.struct".pack; -local s_unpack = require"util.struct".unpack; +local s_pack = require"prosody.util.struct".pack; +local s_unpack = require"prosody.util.struct".unpack; local function pack_uint16be(x) return s_pack(">I2", x); @@ -77,7 +77,6 @@ end -- XORs the string `str` with the array of bytes `key` --- TODO: optimize local function apply_mask(str, key, from, to) return sxor(str:sub(from or 1, to or -1), key); end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/adhoc/adhoc.lib.lua --- a/plugins/adhoc/adhoc.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/adhoc/adhoc.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -4,7 +4,7 @@ -- COPYING file in the source package for more information. -- -local st, uuid = require "util.stanza", require "util.uuid"; +local st, uuid = require "prosody.util.stanza", require "prosody.util.uuid"; local xmlns_cmd = "http://jabber.org/protocol/commands"; @@ -23,9 +23,15 @@ function _M.new(name, node, handler, permission) if not permission then error "adhoc.new() expects a permission argument, none given" + elseif permission == "user" then + error "the permission mode 'user' has been renamed 'any', please update your code" end - if permission == "user" then - error "the permission mode 'user' has been renamed 'any', please update your code" + if permission == "admin" then + module:default_permission("prosody:admin", "adhoc:"..node); + permission = "check"; + elseif permission == "global_admin" then + module:default_permission("prosody:operator", "adhoc:"..node); + permission = "check"; end return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = permission }; end @@ -34,6 +40,8 @@ local cmdtag = stanza.tags[1] local sessionid = cmdtag.attr.sessionid or uuid.generate(); local dataIn = { + origin = origin; + stanza = stanza; to = stanza.attr.to; from = stanza.attr.from; action = cmdtag.attr.action or "execute"; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/adhoc/mod_adhoc.lua --- a/plugins/adhoc/mod_adhoc.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/adhoc/mod_adhoc.lua Wed Mar 27 15:39:03 2024 +0000 @@ -5,28 +5,26 @@ -- COPYING file in the source package for more information. -- -local it = require "util.iterators"; -local st = require "util.stanza"; -local is_admin = require "core.usermanager".is_admin; -local jid_host = require "util.jid".host; +local it = require "prosody.util.iterators"; +local st = require "prosody.util.stanza"; +local jid_host = require "prosody.util.jid".host; local adhoc_handle_cmd = module:require "adhoc".handle_cmd; local xmlns_cmd = "http://jabber.org/protocol/commands"; local commands = {}; module:add_feature(xmlns_cmd); +local function check_permissions(event, node, command, execute) + return (command.permission == "check" and module:may("adhoc:"..node, event, not execute)) + or (command.permission == "local_user" and jid_host(event.stanza.attr.from) == module.host) + or (command.permission == "any"); +end + module:hook("host-disco-info-node", function (event) local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; if commands[node] then - local from = stanza.attr.from; - local privileged = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); local command = commands[node]; - if (command.permission == "admin" and privileged) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "local_user" and hostname == module.host) - or (command.permission == "any") then + if check_permissions(event, node, command) then reply:tag("identity", { name = command.name, category = "automation", type = "command-node" }):up(); reply:tag("feature", { var = xmlns_cmd }):up(); @@ -44,20 +42,13 @@ end); module:hook("host-disco-items-node", function (event) - local stanza, reply, disco_node = event.stanza, event.reply, event.node; + local reply, disco_node = event.reply, event.node; if disco_node ~= xmlns_cmd then return; end - local from = stanza.attr.from; - local admin = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); for node, command in it.sorted_pairs(commands) do - if (command.permission == "admin" and admin) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "local_user" and hostname == module.host) - or (command.permission == "any") then + if check_permissions(event, node, command) then reply:tag("item", { name = command.name, node = node, jid = module:get_host() }); reply:up(); @@ -71,20 +62,14 @@ local node = stanza.tags[1].attr.node local command = commands[node]; if command then - local from = stanza.attr.from; - local admin = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); - if (command.permission == "admin" and not admin) - or (command.permission == "global_admin" and not global_admin) - or (command.permission == "local_user" and hostname ~= module.host) then + if not check_permissions(event, node, command, true) then origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() - :add_child(commands[node]:cmdtag("canceled") + :add_child(command:cmdtag("canceled") :tag("note", {type="error"}):text("You don't have permission to execute this command"))); return true end -- User has permission now execute the command - adhoc_handle_cmd(commands[node], origin, stanza); + adhoc_handle_cmd(command, origin, stanza); return true; end end, 500); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_admin_adhoc.lua --- a/plugins/mod_admin_adhoc.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_admin_adhoc.lua Wed Mar 27 15:39:03 2024 +0000 @@ -14,23 +14,25 @@ local module_host = module:get_host(); -local keys = require "util.iterators".keys; -local usermanager_user_exists = require "core.usermanager".user_exists; -local usermanager_create_user = require "core.usermanager".create_user; -local usermanager_delete_user = require "core.usermanager".delete_user; -local usermanager_set_password = require "core.usermanager".set_password; -local hostmanager_activate = require "core.hostmanager".activate; -local hostmanager_deactivate = require "core.hostmanager".deactivate; -local rm_load_roster = require "core.rostermanager".load_roster; -local st, jid = require "util.stanza", require "util.jid"; -local timer_add_task = require "util.timer".add_task; -local dataforms_new = require "util.dataforms".new; -local array = require "util.array"; -local modulemanager = require "core.modulemanager"; +local keys = require "prosody.util.iterators".keys; +local usermanager_user_exists = require "prosody.core.usermanager".user_exists; +local usermanager_create_user = require "prosody.core.usermanager".create_user; +local usermanager_delete_user = require "prosody.core.usermanager".delete_user; +local usermanager_disable_user = require "prosody.core.usermanager".disable_user; +local usermanager_enable_user = require "prosody.core.usermanager".enable_user; +local usermanager_set_password = require "prosody.core.usermanager".set_password; +local hostmanager_activate = require "prosody.core.hostmanager".activate; +local hostmanager_deactivate = require "prosody.core.hostmanager".deactivate; +local rm_load_roster = require "prosody.core.rostermanager".load_roster; +local st, jid = require "prosody.util.stanza", require "prosody.util.jid"; +local timer_add_task = require "prosody.util.timer".add_task; +local dataforms_new = require "prosody.util.dataforms".new; +local array = require "prosody.util.array"; +local modulemanager = require "prosody.core.modulemanager"; local core_post_stanza = prosody.core_post_stanza; -local adhoc_simple = require "util.adhoc".new_simple_form; -local adhoc_initial = require "util.adhoc".new_initial_data_form; -local set = require"util.set"; +local adhoc_simple = require "prosody.util.adhoc".new_simple_form; +local adhoc_initial = require "prosody.util.adhoc".new_initial_data_form; +local set = require"prosody.util.set"; module:depends("adhoc"); local adhoc_new = module:require "adhoc".new; @@ -152,6 +154,66 @@ "The following accounts could not be deleted:\n"..t_concat(failed, "\n") or "") }; end); +local disable_user_layout = dataforms_new{ + title = "Disabling a User"; + instructions = "Fill out this form to disable a user."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjids", type = "jid-multi", required = true, label = "The Jabber ID(s) to disable" }; +}; + +local disable_user_command_handler = adhoc_simple(disable_user_layout, function(fields, err, data) + if err then + return generate_error_message(err); + end + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host = jid.split(aJID); + if (host == module_host) and usermanager_user_exists(username, host) and usermanager_disable_user(username, host) then + module:log("info", "User %s has been disabled by %s", aJID, jid.bare(data.from)); + succeeded[#succeeded+1] = aJID; + else + module:log("debug", "Tried to disable non-existent user %s", aJID); + failed[#failed+1] = aJID; + end + end + return {status = "completed", info = (#succeeded ~= 0 and + "The following accounts were successfully disabled:\n"..t_concat(succeeded, "\n").."\n" or "").. + (#failed ~= 0 and + "The following accounts could not be disabled:\n"..t_concat(failed, "\n") or "") }; +end); + +local enable_user_layout = dataforms_new{ + title = "Re-Enable a User"; + instructions = "Fill out this form to enable a user."; + + { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" }; + { name = "accountjids", type = "jid-multi", required = true, label = "The Jabber ID(s) to re-enable" }; +}; + +local enable_user_command_handler = adhoc_simple(enable_user_layout, function(fields, err, data) + if err then + return generate_error_message(err); + end + local failed = {}; + local succeeded = {}; + for _, aJID in ipairs(fields.accountjids) do + local username, host = jid.split(aJID); + if (host == module_host) and usermanager_user_exists(username, host) and usermanager_enable_user(username, host) then + module:log("info", "User %s has been enabled by %s", aJID, jid.bare(data.from)); + succeeded[#succeeded+1] = aJID; + else + module:log("debug", "Tried to enable non-existent user %s", aJID); + failed[#failed+1] = aJID; + end + end + return {status = "completed", info = (#succeeded ~= 0 and + "The following accounts were successfully enabled:\n"..t_concat(succeeded, "\n").."\n" or "").. + (#failed ~= 0 and + "The following accounts could not be enabled:\n"..t_concat(failed, "\n") or "") }; +end); + -- Ending a user's session local function disconnect_user(match_jid) local node, hostname, givenResource = jid.split(match_jid); @@ -804,6 +866,8 @@ local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin"); local config_reload_desc = adhoc_new("Reload configuration", "http://prosody.im/protocol/config#reload", config_reload_handler, "global_admin"); local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin"); +local disable_user_desc = adhoc_new("Disable User", "http://jabber.org/protocol/admin#disable-user", disable_user_command_handler, "admin"); +local enable_user_desc = adhoc_new("Re-Enable User", "http://jabber.org/protocol/admin#reenable-user", enable_user_command_handler, "admin"); local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin"); local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin"); local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin"); @@ -824,6 +888,8 @@ module:provides("adhoc", change_user_password_desc); module:provides("adhoc", config_reload_desc); module:provides("adhoc", delete_user_desc); +module:provides("adhoc", disable_user_desc); +module:provides("adhoc", enable_user_desc); module:provides("adhoc", end_user_session_desc); module:provides("adhoc", get_user_roster_desc); module:provides("adhoc", get_user_stats_desc); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_admin_shell.lua --- a/plugins/mod_admin_shell.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_admin_shell.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,38 +10,41 @@ module:set_global(); module:depends("admin_socket"); -local hostmanager = require "core.hostmanager"; -local modulemanager = require "core.modulemanager"; -local s2smanager = require "core.s2smanager"; -local portmanager = require "core.portmanager"; -local helpers = require "util.helpers"; -local server = require "net.server"; -local st = require "util.stanza"; +local hostmanager = require "prosody.core.hostmanager"; +local modulemanager = require "prosody.core.modulemanager"; +local s2smanager = require "prosody.core.s2smanager"; +local portmanager = require "prosody.core.portmanager"; +local helpers = require "prosody.util.helpers"; +local it = require "prosody.util.iterators"; +local server = require "prosody.net.server"; +local schema = require "prosody.util.jsonschema"; +local st = require "prosody.util.stanza"; local _G = _G; local prosody = _G.prosody; -local unpack = table.unpack or unpack; -- luacheck: ignore 113 -local iterators = require "util.iterators"; +local unpack = table.unpack; +local iterators = require "prosody.util.iterators"; local keys, values = iterators.keys, iterators.values; -local jid_bare, jid_split, jid_join = import("util.jid", "bare", "prepped_split", "join"); -local set, array = require "util.set", require "util.array"; -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 serialization = require "util.serialization"; +local jid_bare, jid_split, jid_join, jid_resource, jid_compare = import("prosody.util.jid", "bare", "prepped_split", "join", "resource", "compare"); +local set, array = require "prosody.util.set", require "prosody.util.array"; +local cert_verify_identity = require "prosody.util.x509".verify_identity; +local envload = require "prosody.util.envload".envload; +local envloadfile = require "prosody.util.envload".envloadfile; +local has_pposix, pposix = pcall(require, "prosody.util.pposix"); +local async = require "prosody.util.async"; +local serialization = require "prosody.util.serialization"; local serialize_config = serialization.new ({ fatal = false, unquoted = true}); -local time = require "util.time"; -local promise = require "util.promise"; +local time = require "prosody.util.time"; +local promise = require "prosody.util.promise"; +local logger = require "prosody.util.logger"; local t_insert = table.insert; local t_concat = table.concat; -local format_number = require "util.human.units".format; -local format_table = require "util.human.io".table; +local format_number = require "prosody.util.human.units".format; +local format_table = require "prosody.util.human.io".table; local function capitalize(s) if not s then return end @@ -62,6 +65,86 @@ local def_env = module:shared("env"); local default_env_mt = { __index = def_env }; +local function new_section(section_desc) + return setmetatable({}, { + help = { + desc = section_desc; + commands = {}; + }; + }); +end + +local help_topics = {}; +local function help_topic(name) + return function (desc) + return function (content) + help_topics[name] = { + desc = desc; + content = content; + }; + end; + end +end + +-- Seed with default sections and their description text +help_topic "console" "Help regarding the console itself" [[ +Hey! Welcome to Prosody's admin console. +First thing, if you're ever wondering how to get out, simply type 'quit'. +Secondly, note that we don't support the full telnet protocol yet (it's coming) +so you may have trouble using the arrow keys, etc. depending on your system. + +For now we offer a couple of handy shortcuts: +!! - Repeat the last command +!old!new! - repeat the last command, but with 'old' replaced by 'new' + +For those well-versed in Prosody's internals, or taking instruction from those who are, +you can prefix a command with > to escape the console sandbox, and access everything in +the running server. Great fun, but be careful not to break anything :) +]]; + +local available_columns; --forward declaration so it is reachable from the help + +help_topic "columns" "Information about customizing session listings" (function (self, print) + print [[The columns shown by c2s:show() and s2s:show() can be customizied via the]] + print [['columns' argument as described here.]] + print [[]] + print [[Columns can be specified either as "id jid ipv" or as {"id", "jid", "ipv"}.]] + print [[Available columns are:]] + local meta_columns = { + { title = "ID"; width = 5 }; + { title = "Column Title"; width = 12 }; + { title = "Description"; width = 12 }; + }; + -- auto-adjust widths + for column, spec in pairs(available_columns) do + meta_columns[1].width = math.max(meta_columns[1].width or 0, #column); + meta_columns[2].width = math.max(meta_columns[2].width or 0, #(spec.title or "")); + meta_columns[3].width = math.max(meta_columns[3].width or 0, #(spec.description or "")); + end + local row = format_table(meta_columns, self.session.width) + print(row()); + for column, spec in iterators.sorted_pairs(available_columns) do + print(row({ column, spec.title, spec.description })); + end + print [[]] + print [[Most fields on the internal session structures can also be used as columns]] + -- Also, you can pass a table column specification directly, with mapper callback and all +end); + +help_topic "roles" "Show information about user roles" [[ +Roles may grant access or restrict users from certain operations. + +Built-in roles are: + prosody:guest - Guest/anonymous user + prosody:registered - Registered user + prosody:member - Provisioned user + prosody:admin - Host administrator + prosody:operator - Server administrator + +Roles can be assigned using the user management commands (see 'help user'). +]]; + + local function redirect_output(target, session) local env = setmetatable({ print = session.print }, { __index = function (_, k) return rawget(target, k); end }); env.dofile = function(name) @@ -83,8 +166,8 @@ self.data.print("Error: "..tostring(err)); end -local function send_repl_output(session, line) - return session.send(st.stanza("repl-output"):text(tostring(line))); +local function send_repl_output(session, line, attr) + return session.send(st.stanza("repl-output", attr):text(tostring(line))); end function console:new_session(admin_session) @@ -99,8 +182,14 @@ end return send_repl_output(admin_session, table.concat(t, "\t")); end; + write = function (t) + return send_repl_output(admin_session, t, { eol = "0" }); + end; serialize = tostring; disconnect = function () admin_session:close(); end; + is_connected = function () + return not not admin_session.conn; + end }; session.env = setmetatable({}, default_env_mt); @@ -126,6 +215,11 @@ session = console:new_session(event.origin); event.origin.shell_session = session; end + + local default_width = 132; -- The common default of 80 is a bit too narrow for e.g. s2s:show(), 132 was another common width for hardware terminals + local margin = 2; -- To account for '| ' when lines are printed + session.width = (tonumber(event.stanza.attr.width) or default_width)-margin; + local line = event.stanza:get_text(); local useglobalenv; @@ -135,7 +229,7 @@ line = line:gsub("^>", ""); useglobalenv = true; else - local command = line:match("^%w+") or line:match("%p"); + local command = line:match("^(%w+) ") or line:match("^%w+$") or line:match("%p"); if commands[command] then commands[command](session, line); event.origin.send(result); @@ -201,148 +295,50 @@ return true; end); +local function describe_command(s) + local section, name, args, desc = s:match("^([%w_]+):([%w_]+)%(([^)]*)%) %- (.+)$"); + if not section then + error("Failed to parse command description: "..s); + end + local command_help = getmetatable(def_env[section]).help.commands; + command_help[name] = { + desc = desc; + args = array.collect(args:gmatch("[%w_]+")):map(function (arg_name) + return { name = arg_name }; + end); + }; +end + -- Console commands -- -- These are simple commands, not valid standalone in Lua -local available_columns; --forward declaration so it is reachable from the help - +-- Help about individual topics is handled by def_env.help function commands.help(session, data) local print = session.print; - local section = data:match("^help (%w+)"); - if not section then - print [[Commands are divided into multiple sections. For help on a particular section, ]] - print [[type: help SECTION (for example, 'help c2s'). Sections are: ]] - print [[]] - local row = format_table({ { title = "Section"; width = 7 }; { title = "Description"; width = "100%" } }) - print(row()) - print(row { "c2s"; "Commands to manage local client-to-server sessions" }) - print(row { "s2s"; "Commands to manage sessions between this server and others" }) - print(row { "http"; "Commands to inspect HTTP services" }) -- XXX plural but there is only one so far - print(row { "module"; "Commands to load/reload/unload modules/plugins" }) - print(row { "host"; "Commands to activate, deactivate and list virtual hosts" }) - print(row { "user"; "Commands to create and delete users, and change their passwords" }) - print(row { "roles"; "Show information about user roles" }) - print(row { "muc"; "Commands to create, list and manage chat rooms" }) - print(row { "stats"; "Commands to show internal statistics" }) - print(row { "server"; "Uptime, version, shutting down, etc." }) - print(row { "port"; "Commands to manage ports the server is listening on" }) - print(row { "dns"; "Commands to manage and inspect the internal DNS resolver" }) - print(row { "xmpp"; "Commands for sending XMPP stanzas" }) - print(row { "debug"; "Commands for debugging the server" }) - print(row { "config"; "Reloading the configuration, etc." }) - print(row { "columns"; "Information about customizing session listings" }) - print(row { "console"; "Help regarding the console itself" }) - elseif section == "c2s" then - print [[c2s:show(jid, columns) - Show all client sessions with the specified JID (or all if no JID given)]] - print [[c2s:show_tls(jid) - Show TLS cipher info for encrypted sessions]] - print [[c2s:count() - Count sessions without listing them]] - print [[c2s:close(jid) - Close all sessions for the specified JID]] - print [[c2s:closeall() - Close all active c2s connections ]] - elseif section == "s2s" then - print [[s2s:show(domain, columns) - Show all s2s connections for the given domain (or all if no domain given)]] - print [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]] - print [[s2s:close(from, to) - Close a connection from one domain to another]] - print [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]] - elseif section == "http" then - print [[http:list(hosts) - Show HTTP endpoints]] - elseif section == "module" then - print [[module:info(module, host) - Show information about a loaded module]] - print [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]] - print [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]] - print [[module:unload(module, host) - The same, but just unloads the module from memory]] - print [[module:list(host) - List the modules loaded on the specified host]] - elseif section == "host" then - print [[host:activate(hostname) - Activates the specified host]] - print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]] - print [[host:list() - List the currently-activated hosts]] - elseif section == "user" then - print [[user:create(jid, password, roles) - Create the specified user account]] - print [[user:password(jid, password) - Set the password for the specified user account]] - print [[user:roles(jid, host) - Show current roles for an user]] - print [[user:setroles(jid, host, roles) - Set roles for an user (see 'help roles')]] - print [[user:delete(jid) - Permanently remove the specified user account]] - print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]] - elseif section == "roles" then - print [[Roles may grant access or restrict users from certain operations]] - print [[Built-in roles are:]] - print [[ prosody:admin - Administrator]] - print [[ (empty set) - Normal user]] - print [[]] - print [[The canonical role format looks like: { ["example:role"] = true }]] - print [[For convenience, the following formats are also accepted:]] - print [["admin" - short for "prosody:admin", the normal admin status (like the admins config option)]] - print [["example:role" - short for {["example:role"]=true}]] - print [[{"example:role"} - short for {["example:role"]=true}]] - elseif section == "muc" then - -- TODO `muc:room():foo()` commands - print [[muc:create(roomjid, { config }) - Create the specified MUC room with the given config]] - print [[muc:list(host) - List rooms on the specified MUC component]] - print [[muc:room(roomjid) - Reference the specified MUC room to access MUC API methods]] - elseif section == "server" then - print [[server:version() - Show the server's version number]] - print [[server:uptime() - Show how long the server has been running]] - print [[server:memory() - Show details about the server's memory usage]] - print [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]] - elseif section == "port" then - print [[port:list() - Lists all network ports prosody currently listens on]] - print [[port:close(port, interface) - Close a port]] - elseif section == "dns" then - print [[dns:lookup(name, type, class) - Do a DNS lookup]] - print [[dns:addnameserver(nameserver) - Add a nameserver to the list]] - print [[dns:setnameserver(nameserver) - Replace the list of name servers with the supplied one]] - print [[dns:purge() - Clear the DNS cache]] - print [[dns:cache() - Show cached records]] - elseif section == "xmpp" then - print [[xmpp:ping(localhost, remotehost) -- Sends a ping to a remote XMPP server and reports the response]] - elseif section == "config" then - print [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]] - print [[config:get([host,] option) - Show the value of a config option.]] - elseif section == "stats" then -- luacheck: ignore 542 - print [[stats:show(pattern) - Show internal statistics, optionally filtering by name with a pattern]] - print [[stats:show():cfgraph() - Show a cumulative frequency graph]] - print [[stats:show():histogram() - Show a histogram of selected metric]] - elseif section == "debug" then - print [[debug:logevents(host) - Enable logging of fired events on host]] - print [[debug:events(host, event) - Show registered event handlers]] - print [[debug:timers() - Show information about scheduled timers]] - elseif section == "console" then - print [[Hey! Welcome to Prosody's admin console.]] - print [[First thing, if you're ever wondering how to get out, simply type 'quit'.]] - print [[Secondly, note that we don't support the full telnet protocol yet (it's coming)]] - print [[so you may have trouble using the arrow keys, etc. depending on your system.]] - print [[]] - print [[For now we offer a couple of handy shortcuts:]] - print [[!! - Repeat the last command]] - print [[!old!new! - repeat the last command, but with 'old' replaced by 'new']] - print [[]] - print [[For those well-versed in Prosody's internals, or taking instruction from those who are,]] - print [[you can prefix a command with > to escape the console sandbox, and access everything in]] - print [[the running server. Great fun, but be careful not to break anything :)]] - elseif section == "columns" then - print [[The columns shown by c2s:show() and s2s:show() can be customizied via the]] - print [['columns' argument as described here.]] - print [[]] - print [[Columns can be specified either as "id jid ipv" or as {"id", "jid", "ipv"}.]] - print [[Available columns are:]] - local meta_columns = { - { title = "ID"; width = 5 }; - { title = "Column Title"; width = 12 }; - { title = "Description"; width = 12 }; - }; - -- auto-adjust widths - for column, spec in pairs(available_columns) do - meta_columns[1].width = math.max(meta_columns[1].width or 0, #column); - meta_columns[2].width = math.max(meta_columns[2].width or 0, #(spec.title or "")); - meta_columns[3].width = math.max(meta_columns[3].width or 0, #(spec.description or "")); - end - local row = format_table(meta_columns, 120) - print(row()); - for column, spec in iterators.sorted_pairs(available_columns) do - print(row({ column, spec.title, spec.description })); - end - print [[]] - print [[Most fields on the internal session structures can also be used as columns]] - -- Also, you can pass a table column specification directly, with mapper callback and all + + local topic = data:match("^help (%w+)"); + if topic then + return def_env.help[topic]({ session = session }); + end + + print [[Commands are divided into multiple sections. For help on a particular section, ]] + print [[type: help SECTION (for example, 'help c2s'). Sections are: ]] + print [[]] + local row = format_table({ { title = "Section", width = 7 }, { title = "Description", width = "100%" } }, session.width) + print(row()) + for section_name, section in it.sorted_pairs(def_env) do + local section_mt = getmetatable(section); + local section_help = section_mt and section_mt.help; + print(row { section_name; section_help and section_help.desc or "" }); + end + + print(""); + + print [[In addition to info about commands, the following general topics are available:]] + + print(""); + for topic_name, topic_info in it.sorted_pairs(help_topics) do + print(topic_name .. " - "..topic_info.desc); end end @@ -350,10 +346,13 @@ -- Anything in def_env will be accessible within the session as a global variable --luacheck: ignore 212/self -local serialize_defaults = module:get_option("console_prettyprint_settings", - { fatal = false; unquoted = true; maxdepth = 2; table_iterator = "pairs" }) +local serialize_defaults = module:get_option("console_prettyprint_settings", { + preset = "pretty"; + maxdepth = 2; + table_iterator = "pairs"; +}) -def_env.output = {}; +def_env.output = new_section("Configure admin console output"); function def_env.output:configure(opts) if type(opts) ~= "table" then opts = { preset = opts }; @@ -375,7 +374,57 @@ self.session.serialize = serialization.new(opts); end -def_env.server = {}; +def_env.help = setmetatable({}, { + help = { + desc = "Show this help about available commands"; + commands = {}; + }; + __index = function (_, section_name) + return function (self) + local print = self.session.print; + local section_mt = getmetatable(def_env[section_name]); + local section_help = section_mt and section_mt.help; + + local c = 0; + + if section_help then + print("Help: "..section_name); + if section_help.desc then + print(section_help.desc); + end + print(("-"):rep(#(section_help.desc or section_name))); + print(""); + + if section_help.content then + print(section_help.content); + print(""); + end + + for command, command_help in it.sorted_pairs(section_help.commands or {}) do + c = c + 1; + local args = command_help.args:pluck("name"):concat(", "); + local desc = command_help.desc or command_help.module and ("Provided by mod_"..command_help.module) or ""; + print(("%s:%s(%s) - %s"):format(section_name, command, args, desc)); + end + elseif help_topics[section_name] then + local topic = help_topics[section_name]; + if type(topic.content) == "function" then + topic.content(self, print); + else + print(topic.content); + end + print(""); + return true, "Showing help topic '"..section_name.."'"; + else + print("Unknown topic: "..section_name); + end + print(""); + return true, ("%d command(s) listed"):format(c); + end; + end; +}); + +def_env.server = new_section("Uptime, version, shutting down, etc."); function def_env.server:insane_reload() prosody.unlock_globals(); @@ -384,10 +433,12 @@ return true, "Server reloaded"; end +describe_command [[server:version() - Show the server's version number]] function def_env.server:version() return true, tostring(prosody.version or "unknown"); end +describe_command [[server:uptime() - Show how long the server has been running]] function def_env.server:uptime() local t = os.time()-prosody.start_time; local seconds = t%60; @@ -402,6 +453,7 @@ minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time)); end +describe_command [[server:shutdown(reason) - Shut down the server, with an optional reason to be broadcast to all connections]] function def_env.server:shutdown(reason, code) prosody.shutdown(reason, code); return true, "Shutdown initiated"; @@ -411,6 +463,7 @@ return format_number(kb*1024, "B", "b"); end +describe_command [[server:memory() - Show details about the server's memory usage]] function def_env.server:memory() if not has_pposix or not pposix.meminfo then return true, "Lua is using "..human(collectgarbage("count")); @@ -423,7 +476,7 @@ return true, "OK"; end -def_env.module = {}; +def_env.module = new_section("Commands to load/reload/unload modules/plugins"); local function get_hosts_set(hosts) if type(hosts) == "table" then @@ -469,6 +522,7 @@ return hosts_set; end +describe_command [[module:info(module, host) - Show information about a loaded module]] function def_env.module:info(name, hosts) if not name then return nil, "module name expected"; @@ -481,6 +535,16 @@ local function item_name(item) return item.name; end + local function task_timefmt(t) + if not t then + return "no last run time" + elseif os.difftime(os.time(), t) < 86400 then + return os.date("last run today at %H:%M", t); + else + return os.date("last run %A at %H:%M", t); + end + end + local friendly_descriptions = { ["adhoc-provider"] = "Ad-hoc commands", ["auth-provider"] = "Authentication provider", @@ -498,12 +562,22 @@ ["auth-provider"] = item_name, ["storage-provider"] = item_name, ["http-provider"] = function(item, mod) return mod:http_url(item.name, item.default_path); end, - ["net-provider"] = item_name, + ["net-provider"] = function(item) + local service_name = item.name; + local ports_list = {}; + for _, interface, port in portmanager.get_active_services():iter(service_name, nil, nil) do + table.insert(ports_list, "["..interface.."]:"..port); + end + if not ports_list[1] then + return service_name..": not listening on any ports"; + end + return service_name..": "..table.concat(ports_list, ", "); + end, ["measure"] = function(item) return item.name .. " (" .. suf(item.conf and item.conf.unit, " ") .. item.type .. ")"; end, ["metric"] = function(item) return ("%s (%s%s)%s"):format(item.name, suf(item.mf.unit, " "), item.mf.type_, pre(": ", item.mf.description)); end, - ["task"] = function (item) return string.format("%s (%s)", item.name or item.id, item.when); end + ["task"] = function (item) return string.format("%s (%s, %s)", item.name or item.id, item.when, task_timefmt(item.last)); end }; for host in hosts do @@ -533,21 +607,37 @@ if mod.module.dependencies and next(mod.module.dependencies) ~= nil then print(" dependencies:"); for dep in pairs(mod.module.dependencies) do - print(" - mod_" .. dep); + -- Dependencies are per module instance, not per host, so dependencies + -- of/on global modules may list modules not actually loaded on the + -- current host. + if modulemanager.is_loaded(host, dep) then + print(" - mod_" .. dep); + end + end + end + if mod.module.reverse_dependencies and next(mod.module.reverse_dependencies) ~= nil then + print(" reverse dependencies:"); + for dep in pairs(mod.module.reverse_dependencies) do + if modulemanager.is_loaded(host, dep) then + print(" - mod_" .. dep); + end end end end return true; end -function def_env.module:load(name, hosts, config) +describe_command [[module:load(module, host) - Load the specified module on the specified host (or all hosts if none given)]] +function def_env.module:load(name, hosts) hosts = get_hosts_with_module(hosts); -- Load the module for each host local ok, err, count, mod = true, nil, 0; for host in hosts do + local configured_modules, component = modulemanager.get_modules_for_host(host); + if (not modulemanager.is_loaded(host, name)) then - mod, err = modulemanager.load(host, name, config); + mod, err = modulemanager.load(host, name); if not mod then ok = false; if err == "global-module-already-loaded" then @@ -560,6 +650,10 @@ else count = count + 1; self.session.print("Loaded for "..mod.module.host); + + if not (configured_modules:contains(name) or name == component) then + self.session.print("Note: Module will not be loaded after restart unless enabled in configuration"); + end end end end @@ -567,12 +661,15 @@ return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); end +describe_command [[module:unload(module, host) - The same, but just unloads the module from memory]] function def_env.module:unload(name, hosts) hosts = get_hosts_with_module(hosts, name); -- Unload the module for each host local ok, err, count = true, nil, 0; for host in hosts do + local configured_modules, component = modulemanager.get_modules_for_host(host); + if modulemanager.is_loaded(host, name) then ok, err = modulemanager.unload(host, name); if not ok then @@ -581,6 +678,10 @@ else count = count + 1; self.session.print("Unloaded from "..host); + + if configured_modules:contains(name) or name == component then + self.session.print("Note: Module will be loaded after restart unless disabled in configuration"); + end end end end @@ -593,6 +694,7 @@ else return a:gsub("[^.]+", string.reverse):reverse() < b:gsub("[^.]+", string.reverse):reverse(); end end +describe_command [[module:reload(module, host) - The same, but unloads and loads the module (saving state if the module supports it)]] function def_env.module:reload(name, hosts) hosts = array.collect(get_hosts_with_module(hosts, name)):sort(_sort_hosts) @@ -616,6 +718,7 @@ return ok, (ok and "Module reloaded on "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); end +describe_command [[module:list(host) - List the modules loaded on the specified host]] function def_env.module:list(hosts) hosts = array.collect(set.new({ not hosts and "*" or nil }) + get_hosts_set(hosts)):sort(_sort_hosts); @@ -642,9 +745,10 @@ end end -def_env.config = {}; +def_env.config = new_section("Reloading the configuration, etc."); + function def_env.config:load(filename, format) - local config_load = require "core.configmanager".load; + local config_load = require "prosody.core.configmanager".load; local ok, err = config_load(filename, format); if not ok then return false, err or "Unknown error loading config"; @@ -652,20 +756,30 @@ return true, "Config loaded"; end +describe_command [[config:get([host,] option) - Show the value of a config option.]] function def_env.config:get(host, key) if key == nil then host, key = "*", host; end - local config_get = require "core.configmanager".get + local config_get = require "prosody.core.configmanager".get return true, serialize_config(config_get(host, key)); end +describe_command [[config:set([host,] option, value) - Update the value of a config option without writing to the config file.]] +function def_env.config:set(host, key, value) + if host ~= "*" and not prosody.hosts[host] then + host, key, value = "*", host, key; + end + return require "prosody.core.configmanager".set(host, key, value); +end + +describe_command [[config:reload() - Reload the server configuration. Modules may need to be reloaded for changes to take effect.]] function def_env.config:reload() local ok, err = prosody.reload_config(); return ok, (ok and "Config reloaded (you may need to reload modules to take effect)") or tostring(err); end -def_env.c2s = {}; +def_env.c2s = new_section("Commands to manage local client-to-server sessions"); local function get_jid(session) if session.username then @@ -702,6 +816,7 @@ end); end +describe_command [[c2s:count() - Count sessions without listing them]] function def_env.c2s:count() local c2s = get_c2s(); return true, "Total: ".. #c2s .." clients"; @@ -719,7 +834,7 @@ jid = { title = "JID"; description = "Full JID of user session"; - width = 32; + width = "3p"; key = "full_jid"; mapper = function(full_jid, session) return full_jid or get_jid(session) end; }; @@ -727,7 +842,7 @@ title = "Host"; description = "Local hostname"; key = "host"; - width = 22; + width = "1p"; mapper = function(host, session) return host or get_s2s_hosts(session) or "?"; end; @@ -735,7 +850,7 @@ remote = { title = "Remote"; description = "Remote hostname"; - width = 22; + width = "1p"; mapper = function(_, session) return select(2, get_s2s_hosts(session)); end; @@ -743,7 +858,7 @@ port = { title = "Port"; description = "Server port used"; - width = 5; + width = #string.format("%d", 0xffff); -- max 16 bit unsigned integer align = "right"; key = "conn"; mapper = function(conn) @@ -755,7 +870,7 @@ dir = { title = "Dir"; description = "Direction of server-to-server connection"; - width = 3; + width = #"<->"; key = "direction"; mapper = function(dir, session) if session.incoming and session.outgoing then return "<->"; end @@ -763,12 +878,23 @@ if dir == "incoming" then return "<--"; end end; }; - id = { title = "Session ID"; description = "Internal session ID used in logging"; width = 20; key = "id" }; - type = { title = "Type"; description = "Session type"; width = #"c2s_unauthed"; key = "type" }; + id = { + title = "Session ID"; + description = "Internal session ID used in logging"; + -- Depends on log16(?) of pointers which may vary over runtime, so + some margin + width = math.max(#"c2s", #"s2sin", #"s2sout") + #(tostring({}):match("%x+$")) + 2; + key = "id"; + }; + type = { + title = "Type"; + description = "Session type"; + width = math.max(#"c2s_unauthed", #"s2sout_unauthed"); + key = "type"; + }; method = { title = "Method"; description = "Connection method"; - width = 10; + width = math.max(#"BOSH", #"WebSocket", #"TCP"); mapper = function(_, session) if session.bosh_version then return "BOSH"; @@ -782,15 +908,20 @@ ipv = { title = "IPv"; description = "Internet Protocol version (4 or 6)"; - width = 4; + width = #"IPvX"; key = "ip"; mapper = function(ip) if ip then return ip:find(":") and "IPv6" or "IPv4"; end end; }; - ip = { title = "IP address"; description = "IP address the session connected from"; width = 40; key = "ip" }; + ip = { + title = "IP address"; + description = "IP address the session connected from"; + width = module:get_option_boolean("use_ipv6", true) and #"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" or #"198.051.100.255"; + key = "ip"; + }; status = { title = "Status"; description = "Presence status"; - width = 6; + width = math.max(#"online", #"chat"); key = "presence"; mapper = function(p) if not p then return ""; end @@ -801,24 +932,22 @@ title = "Security"; description = "TLS version or security status"; key = "conn"; - width = 8; + width = math.max(#"secure", #"TLSvX.Y"); mapper = function(conn, session) if not session.secure then return "insecure"; end if not conn or not conn:ssl() then return "secure" end - local sock = conn and conn:socket(); - if not sock then return "secure"; end - local tls_info = sock.info and sock:info(); + local tls_info = conn.ssl_info and conn:ssl_info(); return tls_info and tls_info.protocol or "secure"; end; }; encryption = { title = "Encryption"; description = "Encryption algorithm used (TLS cipher suite)"; - width = 30; + -- openssl ciphers 'ALL:COMPLEMENTOFALL' | tr : \\n | awk 'BEGIN {n=1} length() > n {n=length()} END {print(n)}' + width = #"ECDHE-ECDSA-CHACHA20-POLY1305"; key = "conn"; mapper = function(conn) - local sock = conn and conn:socket(); - local info = sock and sock.info and sock:info(); + local info = conn and conn.ssl_info and conn:ssl_info(); if info then return info.cipher end end; }; @@ -826,19 +955,27 @@ title = "Certificate"; description = "Validation status of certificate"; key = "cert_identity_status"; - width = 11; + width = math.max(#"Expired", #"Self-signed", #"Untrusted", #"Mismatched", #"Unknown"); mapper = function(cert_status, session) - if cert_status then return capitalize(cert_status); end - if session.cert_chain_status == "invalid" then + if cert_status == "invalid" then + -- non-nil cert_identity_status implies valid chain, which covers just + -- about every error condition except mismatched certificate names + return "Mismatched"; + elseif cert_status then + -- basically only "valid" + return capitalize(cert_status); + end + -- no certificate status, + if session.cert_chain_errors then local cert_errors = set.new(session.cert_chain_errors[1]); if cert_errors:contains("certificate has expired") then return "Expired"; elseif cert_errors:contains("self signed certificate") then return "Self-signed"; end + -- Some other cert issue, or something up the chain + -- TODO borrow more logic from mod_s2s/friendly_cert_error() return "Untrusted"; - elseif session.cert_identity_status == "invalid" then - return "Mismatched"; end return "Unknown"; end; @@ -846,7 +983,7 @@ sni = { title = "SNI"; description = "Hostname requested in TLS"; - width = 22; + width = "1p"; -- same as host, remote etc mapper = function(_, session) if not session.conn then return end local sock = session.conn:socket(); @@ -856,7 +993,7 @@ alpn = { title = "ALPN"; description = "Protocol requested in TLS"; - width = 11; + width = math.max(#"http/1.1", #"xmpp-client", #"xmpp-server"); mapper = function(_, session) if not session.conn then return end local sock = session.conn:socket(); @@ -867,7 +1004,8 @@ title = "SM"; description = "Stream Management (XEP-0198) status"; key = "smacks"; - width = 11; + -- FIXME shorter synonym for hibernating + width = math.max(#"yes", #"no", #"hibernating"); mapper = function(smacks_xmlns, session) if not smacks_xmlns then return "no"; end if session.hibernating then return "hibernating"; end @@ -901,7 +1039,7 @@ title = "Dialback"; description = "Legacy server verification"; key = "dialback_key"; - width = 13; + width = math.max(#"Not used", #"Not initiated", #"Initiated", #"Completed"); mapper = function (dialback_key, session) if not dialback_key then if session.type == "s2sin" or session.type == "s2sout" then @@ -915,6 +1053,16 @@ end end }; + role = { + title = "Role"; + description = "Session role with 'prosody:' prefix removed"; + width = "1p"; + key = "role"; + mapper = function(role) + local name = role and role.name; + return name and name:match"^prosody:(%w+)" or name; + end; + } }; local function get_colspec(colspec, default) @@ -922,7 +1070,7 @@ local columns = {}; for i, col in pairs(colspec or default) do if type(col) == "string" then - columns[i] = available_columns[col] or { title = capitalize(col); width = 20; key = col }; + columns[i] = available_columns[col] or { title = capitalize(col); width = "1p"; key = col }; elseif type(col) ~= "table" then return false, ("argument %d: expected string|table but got %s"):format(i, type(col)); else @@ -933,14 +1081,15 @@ return columns; end +describe_command [[c2s:show(jid, columns) - Show all client sessions with the specified JID (or all if no JID given)]] function def_env.c2s:show(match_jid, colspec) local print = self.session.print; - local columns = get_colspec(colspec, { "id"; "jid"; "ipv"; "status"; "secure"; "smacks"; "csi" }); - local row = format_table(columns, 120); + local columns = get_colspec(colspec, { "id"; "jid"; "role"; "ipv"; "status"; "secure"; "smacks"; "csi" }); + local row = format_table(columns, self.session.width); local function match(session) local jid = get_jid(session) - return (not match_jid) or jid == match_jid; + return (not match_jid) or match_jid == "*" or jid_compare(jid, match_jid); end local group_by_host = true; @@ -973,6 +1122,7 @@ return true, ("%d c2s sessions shown"):format(total_count); end +describe_command [[c2s:show_tls(jid) - Show TLS cipher info for encrypted sessions]] function def_env.c2s:show_tls(match_jid) return self:show(match_jid, { "jid"; "id"; "secure"; "encryption" }); end @@ -986,6 +1136,7 @@ end end +describe_command [[c2s:close(jid) - Close all sessions for the specified JID]] function def_env.c2s:close(match_jid, text, condition) local count = 0; show_c2s(function (jid, session) @@ -997,6 +1148,7 @@ return true, "Total: "..count.." sessions closed"; end +describe_command [[c2s:closeall() - Close all active c2s connections ]] function def_env.c2s:closeall(text, condition) local count = 0; --luacheck: ignore 212/jid @@ -1008,7 +1160,8 @@ end -def_env.s2s = {}; +def_env.s2s = new_section("Commands to manage sessions between this server and others"); + local function _sort_s2s(a, b) local a_local, a_remote = get_s2s_hosts(a); local b_local, b_remote = get_s2s_hosts(b); @@ -1016,14 +1169,31 @@ return _sort_hosts(a_local or "", b_local or ""); end +local function match_wildcard(match_jid, jid) + -- host == host or (host) == *.(host) or sub(.host) == *(.host) + return jid == match_jid or jid == match_jid:sub(3) or jid:sub(-#match_jid + 1) == match_jid:sub(2); +end + +local function match_s2s_jid(session, match_jid) + local host, remote = get_s2s_hosts(session); + if not match_jid or match_jid == "*" then + return true; + elseif host == match_jid or remote == match_jid then + return true; + elseif match_jid:sub(1, 2) == "*." then + return match_wildcard(match_jid, host) or match_wildcard(match_jid, remote); + end + return false; +end + +describe_command [[s2s:show(domain, columns) - Show all s2s connections for the given domain (or all if no domain given)]] function def_env.s2s:show(match_jid, colspec) local print = self.session.print; local columns = get_colspec(colspec, { "id"; "host"; "dir"; "remote"; "ipv"; "secure"; "s2s_sasl"; "dialback" }); - local row = format_table(columns, 132); + local row = format_table(columns, self.session.width); local function match(session) - local host, remote = get_s2s_hosts(session); - return not match_jid or host == match_jid or remote == match_jid; + return match_s2s_jid(session, match_jid); end local group_by_host = true; @@ -1057,6 +1227,7 @@ return true, ("%d s2s connections shown"):format(total_count); end +describe_command [[s2s:show_tls(domain) - Show TLS cipher info for encrypted sessions]] function def_env.s2s:show_tls(match_jid) return self:show(match_jid, { "id"; "host"; "dir"; "remote"; "secure"; "encryption"; "cert" }); end @@ -1090,7 +1261,7 @@ local print = self.session.print; local s2s_sessions = module:shared"/*/s2s/sessions"; local domain_sessions = set.new(array.collect(values(s2s_sessions))) - /function(session) return (session.to_host == domain or session.from_host == domain) and session or nil; end; + /function(session) return match_s2s_jid(session, domain) and session or nil; end; local cert_set = {}; for session in domain_sessions do local conn = session.conn; @@ -1179,6 +1350,7 @@ .." presented by "..domain.."."); end +describe_command [[s2s:close(from, to) - Close a connection from one domain to another]] function def_env.s2s:close(from, to, text, condition) local print, count = self.session.print, 0; local s2s_sessions = module:shared"/*/s2s/sessions"; @@ -1193,22 +1365,22 @@ end for _, session in pairs(s2s_sessions) do - local id = session.id or (session.type..tostring(session):match("[a-f0-9]+$")); - if (match_id and match_id == id) - or (session.from_host == from and session.to_host == to) then + local id = session.id or (session.type .. tostring(session):match("[a-f0-9]+$")); + if (match_id and match_id == id) or ((from and match_wildcard(from, session.to_host)) or (to and match_wildcard(to, session.to_host))) then print(("Closing connection from %s to %s [%s]"):format(session.from_host, session.to_host, id)); (session.close or s2smanager.destroy_session)(session, build_reason(text, condition)); - count = count + 1 ; + count = count + 1; end end return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end +describe_command [[s2s:closeall(host) - Close all the incoming/outgoing s2s sessions to specified host]] function def_env.s2s:closeall(host, text, condition) local count = 0; local s2s_sessions = module:shared"/*/s2s/sessions"; for _,session in pairs(s2s_sessions) do - if not host or session.from_host == host or session.to_host == host then + if not host or host == "*" or match_s2s_jid(session, host) then session:close(build_reason(text, condition)); count = count + 1; end @@ -1217,37 +1389,42 @@ else return true, "Closed "..count.." s2s session"..((count == 1 and "") or "s"); end end -def_env.host = {}; def_env.hosts = def_env.host; +def_env.host = new_section("Commands to activate, deactivate and list virtual hosts"); +describe_command [[host:activate(hostname) - Activates the specified host]] function def_env.host:activate(hostname, config) return hostmanager.activate(hostname, config); end + +describe_command [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]] function def_env.host:deactivate(hostname, reason) return hostmanager.deactivate(hostname, reason); end +describe_command [[host:list() - List the currently-activated hosts]] function def_env.host:list() local print = self.session.print; local i = 0; - local type; + local host_type; for host, host_session in iterators.sorted_pairs(prosody.hosts, _sort_hosts) do i = i + 1; - type = host_session.type; - if type == "local" then + host_type = host_session.type; + if host_type == "local" then print(host); else - type = module:context(host):get_option_string("component_module", type); - if type ~= "component" then - type = type .. " component"; + host_type = module:context(host):get_option_string("component_module", host_type); + if host_type ~= "component" then + host_type = host_type .. " component"; end - print(("%s (%s)"):format(host, type)); + print(("%s (%s)"):format(host, host_type)); end end return true, i.." hosts"; end -def_env.port = {}; +def_env.port = new_section("Commands to manage ports the server is listening on"); +describe_command [[port:list() - Lists all network ports prosody currently listens on]] function def_env.port:list() local print = self.session.print; local services = portmanager.get_active_services().data; @@ -1266,6 +1443,7 @@ return true, n_services.." services listening on "..n_ports.." ports"; end +describe_command [[port:close(port, interface) - Close a port]] function def_env.port:close(close_port, close_interface) close_port = assert(tonumber(close_port), "Invalid port number"); local n_closed = 0; @@ -1288,7 +1466,7 @@ return true, "Closed "..n_closed.." ports"; end -def_env.muc = {}; +def_env.muc = new_section("Commands to create, list and manage chat rooms"); local console_room_mt = { __index = function (self, k) return self.room[k]; end; @@ -1307,6 +1485,21 @@ return room_name, host; end +local function get_muc(room_jid) + local room_name, host = check_muc(room_jid); + if not room_name then + return room_name, host; + end + local room_obj = prosody.hosts[host].modules.muc.get_room_from_jid(room_jid); + if not room_obj then + return nil, "No such room: "..room_jid; + end + return room_obj; +end + +local muc_util = module:require"muc/util"; + +describe_command [[muc:create(roomjid, { config }) - Create the specified MUC room with the given config]] function def_env.muc:create(room_jid, config) local room_name, host = check_muc(room_jid); if not room_name then @@ -1318,18 +1511,16 @@ return prosody.hosts[host].modules.muc.create_room(room_jid, config); end +describe_command [[muc:room(roomjid) - Reference the specified MUC room to access MUC API methods]] function def_env.muc:room(room_jid) - local room_name, host = check_muc(room_jid); - if not room_name then - return room_name, host; - end - local room_obj = prosody.hosts[host].modules.muc.get_room_from_jid(room_jid); + local room_obj, err = get_muc(room_jid); if not room_obj then - return nil, "No such room: "..room_jid; + return room_obj, err; end return setmetatable({ room = room_obj }, console_room_mt); end +describe_command [[muc:list(host) - List rooms on the specified MUC component]] function def_env.muc:list(host) local host_session = prosody.hosts[host]; if not host_session or not host_session.modules.muc then @@ -1344,36 +1535,160 @@ return true, c.." rooms"; end -local um = require"core.usermanager"; +describe_command [[muc:occupants(roomjid, filter) - List room occupants, optionally filtered on substring or role]] +function def_env.muc:occupants(room_jid, filter) + local room_obj, err = get_muc(room_jid); + if not room_obj then + return room_obj, err; + end -local function coerce_roles(roles) - if roles == "admin" then roles = "prosody:admin"; end - if type(roles) == "string" then roles = { [roles] = true }; end - if roles[1] then for i, role in ipairs(roles) do roles[role], roles[i] = true, nil; end end - return roles; + local print = self.session.print; + local row = format_table({ + { title = "Role"; width = 12; key = "role" }; -- longest role name + { title = "JID"; width = "75%"; key = "bare_jid" }; + { title = "Nickname"; width = "25%"; key = "nick"; mapper = jid_resource }; + }, self.session.width); + local occupants = array.collect(iterators.select(2, room_obj:each_occupant())); + local total = #occupants; + if filter then + occupants:filter(function(occupant) + return occupant.role == filter or jid_resource(occupant.nick):find(filter, 1, true); + end); + end + local displayed = #occupants; + occupants:sort(function(a, b) + if a.role ~= b.role then + return muc_util.valid_roles[a.role] > muc_util.valid_roles[b.role]; + else + return a.bare_jid < b.bare_jid; + end + end); + + if displayed == 0 then + return true, ("%d out of %d occupant%s listed"):format(displayed, total, total ~= 1 and "s" or "") + end + + print(row()); + for _, occupant in ipairs(occupants) do + print(row(occupant)); + end + + if total == displayed then + return true, ("%d occupant%s listed"):format(total, total ~= 1 and "s" or "") + else + return true, ("%d out of %d occupant%s listed"):format(displayed, total, total ~= 1 and "s" or "") + end end -def_env.user = {}; -function def_env.user:create(jid, password, roles) +describe_command [[muc:affiliations(roomjid, filter) - List affiliated members of the room, optionally filtered on substring or affiliation]] +function def_env.muc:affiliations(room_jid, filter) + local room_obj, err = get_muc(room_jid); + if not room_obj then + return room_obj, err; + end + + local print = self.session.print; + local row = format_table({ + { title = "Affiliation"; width = 12 }; -- longest affiliation name + { title = "JID"; width = "75%" }; + { title = "Nickname"; width = "25%"; key = "reserved_nickname" }; + }, self.session.width); + local affiliated = array(); + for affiliated_jid, affiliation, affiliation_data in room_obj:each_affiliation() do + affiliated:push(setmetatable({ affiliation; affiliated_jid }, { __index = affiliation_data })); + end + + local total = #affiliated; + if filter then + affiliated:filter(function(affiliation) + return filter == affiliation[1] or affiliation[2]:find(filter, 1, true); + end); + end + local displayed = #affiliated; + local aff_ranking = muc_util.valid_affiliations; + affiliated:sort(function(a, b) + if a[1] ~= b[1] then + return aff_ranking[a[1]] > aff_ranking[b[1]]; + else + return a[2] < b[2]; + end + end); + + if displayed == 0 then + return true, ("%d out of %d affiliations%s listed"):format(displayed, total, total ~= 1 and "s" or "") + end + + print(row()); + for _, affiliation in ipairs(affiliated) do + print(row(affiliation)); + end + + + if total == displayed then + return true, ("%d affiliation%s listed"):format(total, total ~= 1 and "s" or "") + else + return true, ("%d out of %d affiliation%s listed"):format(displayed, total, total ~= 1 and "s" or "") + end +end + +local um = require"prosody.core.usermanager"; + +def_env.user = new_section("Commands to create and delete users, and change their passwords"); + +describe_command [[user:create(jid, password, role) - Create the specified user account]] +function def_env.user:create(jid, password, role) local username, host = jid_split(jid); if not prosody.hosts[host] then return nil, "No such host: "..host; elseif um.user_exists(username, host) then return nil, "User exists"; end - local ok, err = um.create_user(username, password, host); + + if not role then + role = module:get_option_string("default_provisioned_role", "prosody:member"); + end + + local ok, err = um.create_user_with_role(username, password, host, role); + if not ok then + return nil, "Could not create user: "..err; + end + + return true, ("Created %s with role '%s'"):format(jid, role); +end + +describe_command [[user:disable(jid) - Disable the specified user account, preventing login]] +function def_env.user:disable(jid) + local username, host = jid_split(jid); + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif not um.user_exists(username, host) then + return nil, "No such user"; + end + local ok, err = um.disable_user(username, host); if ok then - if ok and roles then - roles = coerce_roles(roles); - local roles_ok, rerr = um.set_roles(jid, host, roles); - if not roles_ok then return nil, "User created, but could not set roles: " .. tostring(rerr); end - end - return true, "User created"; + return true, "User disabled"; else - return nil, "Could not create user: "..err; + return nil, "Could not disable user: "..err; end end +describe_command [[user:enable(jid) - Enable the specified user account, restoring login access]] +function def_env.user:enable(jid) + local username, host = jid_split(jid); + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif not um.user_exists(username, host) then + return nil, "No such user"; + end + local ok, err = um.enable_user(username, host); + if ok then + return true, "User enabled"; + else + return nil, "Could not enable user: "..err; + end +end + +describe_command [[user:delete(jid) - Permanently remove the specified user account]] function def_env.user:delete(jid) local username, host = jid_split(jid); if not prosody.hosts[host] then @@ -1389,6 +1704,7 @@ end end +describe_command [[user:password(jid, password) - Set the password for the specified user account]] function def_env.user:password(jid, password) local username, host = jid_split(jid); if not prosody.hosts[host] then @@ -1404,43 +1720,71 @@ end end -function def_env.user:roles(jid, host, new_roles) - if new_roles or type(host) == "table" then - return nil, "Use user:setroles(jid, host, roles) to change user roles"; - end +describe_command [[user:roles(jid, host) - Show current roles for an user]] +function def_env.user:role(jid, host) + local print = self.session.print; local username, userhost = jid_split(jid); if host == nil then host = userhost; end - if host ~= "*" and not prosody.hosts[host] then + if not prosody.hosts[host] then return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; end - local roles = um.get_roles(jid, host); - if not roles then return true, "No roles"; end - local count = 0; - local print = self.session.print; - for role in pairs(roles) do + + local primary_role = um.get_user_role(username, host); + local secondary_roles = um.get_user_secondary_roles(username, host); + + print(primary_role and primary_role.name or ""); + + local count = primary_role and 1 or 0; + for role_name in pairs(secondary_roles or {}) do count = count + 1; - print(role); + print(role_name.." (secondary)"); end + return true, count == 1 and "1 role" or count.." roles"; end -def_env.user.showroles = def_env.user.roles; -- COMPAT +def_env.user.roles = def_env.user.role; --- user:roles("someone@example.com", "example.com", {"prosody:admin"}) --- user:roles("someone@example.com", {"prosody:admin"}) -function def_env.user:setroles(jid, host, new_roles) +describe_command [[user:setrole(jid, host, role) - Set primary role of a user (see 'help roles')]] +-- user:setrole("someone@example.com", "example.com", "prosody:admin") +-- user:setrole("someone@example.com", "prosody:admin") +function def_env.user:setrole(jid, host, new_role) local username, userhost = jid_split(jid); - if new_roles == nil then host, new_roles = userhost, host; end - if host ~= "*" and not prosody.hosts[host] then + if new_role == nil then host, new_role = userhost, host; end + if not prosody.hosts[host] then return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; end - if host == "*" then host = nil; end - return um.set_roles(jid, host, coerce_roles(new_roles)); + return um.set_user_role(username, host, new_role); end +describe_command [[user:addrole(jid, host, role) - Add a secondary role to a user]] +function def_env.user:addrole(jid, host, new_role) + local username, userhost = jid_split(jid); + if new_role == nil then host, new_role = userhost, host; end + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then + return nil, "No such user"; + end + return um.add_user_secondary_role(username, host, new_role); +end + +describe_command [[user:delrole(jid, host, role) - Remove a secondary role from a user]] +function def_env.user:delrole(jid, host, role_name) + local username, userhost = jid_split(jid); + if role_name == nil then host, role_name = userhost, host; end + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then + return nil, "No such user"; + end + return um.remove_user_secondary_role(username, host, role_name); +end + +describe_command [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]] -- TODO switch to table view, include roles function def_env.user:list(host, pat) if not host then @@ -1460,9 +1804,10 @@ return true, "Showing "..(pat and (matches.." of ") or "all " )..total.." users"; end -def_env.xmpp = {}; +def_env.xmpp = new_section("Commands for sending XMPP stanzas"); -local new_id = require "util.id".medium; +describe_command [[xmpp:ping(localhost, remotehost) - Sends a ping to a remote XMPP server and reports the response]] +local new_id = require "prosody.util.id".medium; function def_env.xmpp:ping(localhost, remotehost, timeout) localhost = select(2, jid_split(localhost)); remotehost = select(2, jid_split(remotehost)); @@ -1509,12 +1854,12 @@ module:unhook("s2sin-established", onestablished); module:unhook("s2s-destroyed", ondestroyed); end):next(function(pong) - return ("pong from %s in %gs"):format(pong.stanza.attr.from, time.now() - time_start); + return ("pong from %s on %s in %gs"):format(pong.stanza.attr.from, pong.origin.id, time.now() - time_start); end); end -def_env.dns = {}; -local adns = require"net.adns"; +def_env.dns = new_section("Commands to manage and inspect the internal DNS resolver"); +local adns = require"prosody.net.adns"; local function get_resolver(session) local resolver = session.dns_resolver; @@ -1525,43 +1870,54 @@ return resolver; end +describe_command [[dns:lookup(name, type, class) - Do a DNS lookup]] function def_env.dns:lookup(name, typ, class) local resolver = get_resolver(self.session); return resolver:lookup_promise(name, typ, class) end +describe_command [[dns:addnameserver(nameserver) - Add a nameserver to the list]] function def_env.dns:addnameserver(...) local resolver = get_resolver(self.session); resolver._resolver:addnameserver(...) return true end +describe_command [[dns:setnameserver(nameserver) - Replace the list of name servers with the supplied one]] function def_env.dns:setnameserver(...) local resolver = get_resolver(self.session); resolver._resolver:setnameserver(...) return true end +describe_command [[dns:purge() - Clear the DNS cache]] function def_env.dns:purge() local resolver = get_resolver(self.session); resolver._resolver:purge() return true end +describe_command [[dns:cache() - Show cached records]] function def_env.dns:cache() local resolver = get_resolver(self.session); return true, "Cache:\n"..tostring(resolver._resolver.cache) end -def_env.http = {}; +def_env.http = new_section("Commands to inspect HTTP services"); +describe_command [[http:list(hosts) - Show HTTP endpoints]] function def_env.http:list(hosts) local print = self.session.print; hosts = array.collect(set.new({ not hosts and "*" or nil }) + get_hosts_set(hosts)):sort(_sort_hosts); - local output = format_table({ - { title = "Module", width = "20%" }, - { title = "URL", width = "80%" }, - }, 132); + local output_simple = format_table({ + { title = "Module"; width = "1p" }; + { title = "External URL"; width = "6p" }; + }, self.session.width); + local output_split = format_table({ + { title = "Module"; width = "1p" }; + { title = "External URL"; width = "3p" }; + { title = "Internal URL"; width = "3p" }; + }, self.session.width); for _, host in ipairs(hosts) do local http_apps = modulemanager.get_items("http-provider", host); @@ -1572,12 +1928,14 @@ else print("HTTP endpoints on "..host..(http_host and (" (using "..http_host.."):") or ":")); end - print(output()); + print(output_split()); for _, provider in ipairs(http_apps) do local mod = provider._provided_by; - local url = module:context(host):http_url(provider.name, provider.default_path); + local external = module:context(host):http_url(provider.name, provider.default_path); + local internal = module:context(host):http_url(provider.name, provider.default_path, "internal"); + if external==internal then internal="" end mod = mod and "mod_"..mod or "" - print(output{mod, url}); + print((internal=="" and output_simple or output_split){mod, external, internal}); end print(""); end @@ -1592,18 +1950,83 @@ return true; end -def_env.debug = {}; +def_env.watch = new_section("Commands for watching live logs from the server"); + +describe_command [[watch:log() - Follow debug logs]] +function def_env.watch:log() + local writing = false; + local sink = logger.add_simple_sink(function (source, level, message) + if writing then return; end + writing = true; + self.session.print(source, level, message); + writing = false; + end); + + while self.session.is_connected() do + async.sleep(3); + end + if not logger.remove_sink(sink) then + module:log("warn", "Unable to remove watch:log() sink"); + end +end +describe_command [[watch:stanzas(target, filter) - Watch live stanzas matching the specified target and filter]] +local stanza_watchers = module:require("mod_debug_stanzas/watcher"); +function def_env.watch:stanzas(target_spec, filter_spec) + local function handler(event_type, stanza, session) + if stanza then + if event_type == "sent" then + self.session.print(("\n"):format(session.id)); + elseif event_type == "received" then + self.session.print(("\n"):format(session.id)); + else + self.session.print(("\n"):format(event_type, session.id)); + end + self.session.print(stanza); + elseif session then + self.session.print("\n"); + elseif event_type then + self.session.print("\n"); + end + end + + stanza_watchers.add({ + target_spec = { + jid = target_spec; + }; + filter_spec = filter_spec and { + with_jid = filter_spec; + }; + }, handler); + + while self.session.is_connected() do + async.sleep(3); + end + + stanza_watchers.remove(handler); +end + +def_env.debug = new_section("Commands for debugging the server"); + +describe_command [[debug:logevents(host) - Enable logging of fired events on host]] function def_env.debug:logevents(host) - helpers.log_host_events(host); + if host == "*" then + helpers.log_events(prosody.events); + elseif host == "http" then + helpers.log_events(require "prosody.net.http.server"._events); + return true + else + helpers.log_host_events(host); + end return true; end +describe_command [[debug:events(host, event) - Show registered event handlers]] function def_env.debug:events(host, event) local events_obj; if host and host ~= "*" then if host == "http" then - events_obj = require "net.http.server"._events; + events_obj = require "prosody.net.http.server"._events; elseif not prosody.hosts[host] then return false, "Unknown host: "..host; else @@ -1615,9 +2038,10 @@ return true, helpers.show_events(events_obj, event); end +describe_command [[debug:timers() - Show information about scheduled timers]] function def_env.debug:timers() local print = self.session.print; - local add_task = require"util.timer".add_task; + local add_task = require"prosody.util.timer".add_task; local h, params = add_task.h, add_task.params; local function normalize_time(t) return t; @@ -1671,10 +2095,70 @@ return true; end --- COMPAT: debug:timers() was timer:info() for some time in trunk -def_env.timer = { info = def_env.debug.timers }; +describe_command [[debug:async() - Show information about pending asynchronous tasks]] +function def_env.debug:async(runner_id) + local print = self.session.print; + local time_now = time.now(); + + if runner_id then + for runner, since in pairs(async.waiting_runners) do + if runner.id == runner_id then + print("ID ", runner.id); + local f = runner.func; + if f == async.default_runner_func then + print("Function ", tostring(runner.current_item).." (from work queue)"); + else + print("Function ", tostring(f)); + if st.is_stanza(runner.current_item) then + print("Stanza:") + print("\t"..runner.current_item:indent(2):pretty_print()); + else + print("Work item", self.session.serialize(runner.current_item, "debug")); + end + end -def_env.stats = {}; + print("Coroutine ", tostring(runner.thread).." ("..coroutine.status(runner.thread)..")"); + print("Since ", since); + print("Status ", ("%s since %s (%0.2f seconds ago)"):format(runner.state, os.date("%Y-%m-%d %R:%S", math.floor(since)), time_now-since)); + print(""); + print(debug.traceback(runner.thread)); + return true, "Runner is "..runner.state; + end + end + return nil, "Runner not found or is currently idle"; + end + + local row = format_table({ + { title = "ID"; width = 12 }; + { title = "Function"; width = "10p" }; + { title = "Status"; width = "16" }; + { title = "Location"; width = "10p" }; + }, self.session.width); + print(row()) + + local c = 0; + for runner, since in pairs(async.waiting_runners) do + c = c + 1; + local f = runner.func; + if f == async.default_runner_func then + f = runner.current_item; + end + -- We want to fetch the location in the code that the runner yielded from, + -- excluding util.async's wrapper code. A level of `2` assumes that we + -- yielded directly from a function in util.async. This is *currently* true + -- of all util.async yields, but it's fragile. + local location = debug.getinfo(runner.thread, 2); + print(row { + runner.id; + tostring(f); + ("%s (%0.2fs)"):format(runner.state, time_now - since); + location.short_src..(location.currentline and ":"..location.currentline or ""); + }); + end + return true, ("%d runners pending"):format(c); +end + +def_env.stats = new_section("Commands to show internal statistics"); local short_units = { seconds = "s", @@ -1913,8 +2397,10 @@ return setmetatable({ session = self.session, stats = true, now = time.now() }, stats_mt); end +describe_command [[stats:show(pattern) - Show internal statistics, optionally filtering by name with a pattern.]] +-- Undocumented currently, you can append :histogram() or :cfgraph() to stats:show() for rendered graphs. function def_env.stats:show(name_filter) - local statsman = require "core.statsmanager" + local statsman = require "prosody.core.statsmanager" local collect = statsman.collect if collect then -- force collection if in manual mode @@ -1934,6 +2420,176 @@ return displayed_stats; end +local command_metadata_schema = { + type = "object"; + properties = { + section = { type = "string" }; + section_desc = { type = "string" }; + + name = { type = "string" }; + desc = { type = "string" }; + help = { type = "string" }; + args = { + type = "array"; + items = { + type = "object"; + properties = { + name = { type = "string", required = true }; + type = { type = "string", required = false }; + }; + }; + }; + }; + + required = { "name", "section", "desc", "args" }; +}; + +-- host_commands[section..":"..name][host] = handler +-- host_commands[section..":"..name][false] = metadata +local host_commands = {}; + +local function new_item_handlers(command_host) + local function on_command_added(event) + local command = event.item; + local mod_name = command._provided_by and ("mod_"..command._provided_by) or ""; + if not schema.validate(command_metadata_schema, command) or type(command.handler) ~= "function" then + module:log("warn", "Ignoring command added by %s: missing or invalid data", mod_name); + return; + end + + local handler = command.handler; + + if command_host then + if type(command.host_selector) ~= "string" then + module:log("warn", "Ignoring command %s:%s() added by %s - missing/invalid host_selector", command.section, command.name, mod_name); + return; + end + local qualified_name = command.section..":"..command.name; + local host_command_info = host_commands[qualified_name]; + if not host_command_info then + local selector_index; + for i, arg in ipairs(command.args) do + if arg.name == command.host_selector then + selector_index = i + 1; -- +1 to account for 'self' + break; + end + end + if not selector_index then + module:log("warn", "Command %s() host selector argument '%s' not found - not registering", qualified_name, command.host_selector); + return; + end + host_command_info = { + [false] = { + host_selector = command.host_selector; + handler = function (...) + local selected_host = select(2, jid_split((select(selector_index, ...)))); + if type(selected_host) ~= "string" then + return nil, "Invalid or missing argument '"..command.host_selector.."'"; + end + if not prosody.hosts[selected_host] then + return nil, "Unknown host: "..selected_host; + end + local host_handler = host_commands[qualified_name][selected_host]; + if not host_handler then + return nil, "This command is not available on "..selected_host; + end + return host_handler(...); + end; + }; + }; + host_commands[qualified_name] = host_command_info; + end + if host_command_info[command_host] then + module:log("warn", "Command %s() is already registered - overwriting with %s", qualified_name, mod_name); + end + host_command_info[command_host] = handler; + end + + local section_t = def_env[command.section]; + if not section_t then + section_t = {}; + def_env[command.section] = section_t; + end + + if command_host then + section_t[command.name] = host_commands[command.section..":"..command.name][false].handler; + else + section_t[command.name] = command.handler; + end + + local section_mt = getmetatable(section_t); + if not section_mt then + section_mt = {}; + setmetatable(section_t, section_mt); + end + local section_help = section_mt.help; + if not section_help then + section_help = { + desc = command.section_desc; + commands = {}; + }; + section_mt.help = section_help; + end + + section_help.commands[command.name] = { + desc = command.desc; + full = command.help; + args = array(command.args); + module = command._provided_by; + }; + + module:log("debug", "Shell command added by mod_%s: %s:%s()", mod_name, command.section, command.name); + end + + local function on_command_removed(event) + local command = event.item; + + local handler = event.item.handler; + if type(handler) ~= "function" or not schema.validate(command_metadata_schema, command) then + return; + end + + local section_t = def_env[command.section]; + if not section_t or section_t[command.name] ~= handler then + return; + end + + section_t[command.name] = nil; + if next(section_t) == nil then -- Delete section if empty + def_env[command.section] = nil; + end + + if command_host then + local host_command_info = host_commands[command.section..":"..command.name]; + if host_command_info then + -- Remove our host handler + host_command_info[command_host] = nil; + -- Clean up entire command entry if there are no per-host handlers left + local any_hosts = false; + for k in pairs(host_command_info) do + if k then -- metadata is false, ignore it + any_hosts = true; + break; + end + end + if not any_hosts then + host_commands[command.section..":"..command.name] = nil; + end + end + end + end + return on_command_added, on_command_removed; +end + +module:handle_items("shell-command", new_item_handlers()); + +function module.add_host(host_module) + host_module:handle_items("shell-command", new_item_handlers(host_module.host)); +end + +function module.unload() + stanza_watchers.cleanup(); +end ------------- diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_admin_socket.lua --- a/plugins/mod_admin_socket.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_admin_socket.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,7 +8,7 @@ -- constructor was exported instead of a module table. Due to the lack of a -- proper release of LuaSocket, distros have settled on shipping either the -- last RC tag or some commit since then. - -- Here we accomodate both variants. + -- Here we accommodate both variants. unix = { stream = unix }; end if not have_unix or type(unix) ~= "table" then @@ -16,10 +16,10 @@ return; end -local server = require "net.server"; +local server = require "prosody.net.server"; -local adminstream = require "util.adminstream"; -local st = require "util.stanza"; +local adminstream = require "prosody.util.adminstream"; +local st = require "prosody.util.stanza"; local socket_path = module:get_option_path("admin_socket", "prosody.sock", "data"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_admin_telnet.lua --- a/plugins/mod_admin_telnet.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_admin_telnet.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,8 +12,8 @@ local console_listener = { default_port = 5582; default_mode = "*a"; interface = "127.0.0.1" }; -local async = require "util.async"; -local st = require "util.stanza"; +local async = require "prosody.util.async"; +local st = require "prosody.util.stanza"; local def_env = module:shared("admin_shell/env"); local default_env_mt = { __index = def_env }; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_announce.lua --- a/plugins/mod_announce.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_announce.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,10 +6,9 @@ -- COPYING file in the source package for more information. -- -local st, jid = require "util.stanza", require "util.jid"; +local st, jid = require "prosody.util.stanza", require "prosody.util.jid"; local hosts = prosody.hosts; -local is_admin = require "core.usermanager".is_admin; function send_to_online(message, host) local sessions; @@ -34,6 +33,7 @@ return c; end +module:default_permission("prosody:admin", ":send-announcement"); -- Old -based jabberd-style announcement sending function handle_announcement(event) @@ -45,8 +45,8 @@ return; -- Not an announcement end - if not is_admin(stanza.attr.from, host) then - -- Not an admin? Not allowed! + if not module:may(":send-announcement", event) then + -- Not allowed! module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from); return; end @@ -63,7 +63,7 @@ module:hook("message/host", handle_announcement); -- Ad-hoc command (XEP-0133) -local dataforms_new = require "util.dataforms".new; +local dataforms_new = require "prosody.util.dataforms".new; local announce_layout = dataforms_new{ title = "Making an Announcement"; instructions = "Fill out this form to make an announcement to all\nactive users of this service."; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_auth_anonymous.lua --- a/plugins/mod_auth_anonymous.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_auth_anonymous.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,8 +7,8 @@ -- -- luacheck: ignore 212 -local new_sasl = require "util.sasl".new; -local datamanager = require "util.datamanager"; +local new_sasl = require "prosody.util.sasl".new; +local datamanager = require "prosody.util.datamanager"; local hosts = prosody.hosts; local allow_storage = module:get_option_boolean("allow_anonymous_storage", false); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_auth_insecure.lua --- a/plugins/mod_auth_insecure.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_auth_insecure.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,9 +7,9 @@ -- -- luacheck: ignore 212 -local datamanager = require "util.datamanager"; -local new_sasl = require "util.sasl".new; -local saslprep = require "util.encodings".stringprep.saslprep; +local datamanager = require "prosody.util.datamanager"; +local new_sasl = require "prosody.util.sasl".new; +local saslprep = require "prosody.util.encodings".stringprep.saslprep; local host = module.host; local provider = { name = "insecure" }; @@ -27,6 +27,7 @@ return nil, "Password fails SASLprep."; end if account then + account.updated = os.time(); account.password = password; return datamanager.store(username, host, "accounts", account); end @@ -38,7 +39,8 @@ end function provider.create_user(username, password) - return datamanager.store(username, host, "accounts", {password = password}); + local now = os.time(); + return datamanager.store(username, host, "accounts", { created = now; updated = now; password = password }); end function provider.delete_user(username) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_auth_internal_hashed.lua --- a/plugins/mod_auth_internal_hashed.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_auth_internal_hashed.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,26 +9,27 @@ local max = math.max; -local scram_hashers = require "util.sasl.scram".hashers; -local usermanager = require "core.usermanager"; -local generate_uuid = require "util.uuid".generate; -local new_sasl = require "util.sasl".new; -local hex = require"util.hex"; +local scram_hashers = require "prosody.util.sasl.scram".hashers; +local generate_uuid = require "prosody.util.uuid".generate; +local new_sasl = require "prosody.util.sasl".new; +local hex = require"prosody.util.hex"; local to_hex, from_hex = hex.encode, hex.decode; -local saslprep = require "util.encodings".stringprep.saslprep; -local secure_equals = require "util.hashes".equals; +local saslprep = require "prosody.util.encodings".stringprep.saslprep; +local secure_equals = require "prosody.util.hashes".equals; local log = module._log; local host = module.host; local accounts = module:open_store("accounts"); -local hash_name = module:get_option_string("password_hash", "SHA-1"); +local hash_name = module:get_option_enum("password_hash", "SHA-1", "SHA-256"); local get_auth_db = assert(scram_hashers[hash_name], "SCRAM-"..hash_name.." not supported by SASL library"); local scram_name = "scram_"..hash_name:gsub("%-","_"):lower(); -- Default; can be set per-user -local default_iteration_count = module:get_option_number("default_iteration_count", 10000); +local default_iteration_count = module:get_option_integer("default_iteration_count", 10000, 4096); + +local tokenauth = module:depends("tokenauth"); -- define auth provider local provider = {}; @@ -86,11 +87,22 @@ account.server_key = server_key_hex account.password = nil; + account.updated = os.time(); return accounts:set(username, account); end return nil, "Account not available."; end +function provider.get_account_info(username) + local account = accounts:get(username); + if not account then return nil, "Account not available"; end + return { + created = account.created; + password_updated = account.updated; + enabled = not account.disabled; + }; +end + function provider.user_exists(username) local account = accounts:get(username); if not account then @@ -100,13 +112,36 @@ return true; end +function provider.is_enabled(username) -- luacheck: ignore 212 + local info, err = provider.get_account_info(username); + if not info then return nil, err; end + return info.enabled; +end + +function provider.enable(username) + -- TODO map store? + local account = accounts:get(username); + account.disabled = nil; + account.updated = os.time(); + return accounts:set(username, account); +end + +function provider.disable(username, meta) + local account = accounts:get(username); + account.disabled = true; + account.disabled_meta = meta; + account.updated = os.time(); + return accounts:set(username, account); +end + function provider.users() return accounts:users(); end function provider.create_user(username, password) + local now = os.time(); if password == nil then - return accounts:set(username, {}); + return accounts:set(username, { created = now; updated = now; disabled = true }); end local salt = generate_uuid(); local valid, stored_key, server_key = get_auth_db(password, salt, default_iteration_count); @@ -117,7 +152,8 @@ local server_key_hex = to_hex(server_key); return accounts:set(username, { stored_key = stored_key_hex, server_key = server_key_hex, - salt = salt, iteration_count = default_iteration_count + salt = salt, iteration_count = default_iteration_count, + created = now, updated = now; }); end @@ -127,8 +163,8 @@ function provider.get_sasl_handler() local testpass_authentication_profile = { - plain_test = function(_, username, password, realm) - return usermanager.test_password(username, realm, password), true; + plain_test = function(_, username, password) + return provider.test_password(username, password), provider.is_enabled(username); end, [scram_name] = function(_, username) local credentials = accounts:get(username); @@ -145,8 +181,9 @@ local iteration_count, salt = credentials.iteration_count, credentials.salt; stored_key = stored_key and from_hex(stored_key); server_key = server_key and from_hex(server_key); - return stored_key, server_key, iteration_count, salt, true; - end + return stored_key, server_key, iteration_count, salt, not credentials.disabled; + end; + oauthbearer = tokenauth.sasl_handler(provider, "oauth2", module:shared("tokenauth/oauthbearer_config")); }; return new_sasl(host, testpass_authentication_profile); end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_auth_internal_plain.lua --- a/plugins/mod_auth_internal_plain.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_auth_internal_plain.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,10 +6,10 @@ -- COPYING file in the source package for more information. -- -local usermanager = require "core.usermanager"; -local new_sasl = require "util.sasl".new; -local saslprep = require "util.encodings".stringprep.saslprep; -local secure_equals = require "util.hashes".equals; +local usermanager = require "prosody.core.usermanager"; +local new_sasl = require "prosody.util.sasl".new; +local saslprep = require "prosody.util.encodings".stringprep.saslprep; +local secure_equals = require "prosody.util.hashes".equals; local log = module._log; local host = module.host; @@ -48,11 +48,21 @@ local account = accounts:get(username); if account then account.password = password; + account.updated = os.time(); return accounts:set(username, account); end return nil, "Account not available."; end +function provider.get_account_info(username) + local account = accounts:get(username); + if not account then return nil, "Account not available"; end + return { + created = account.created; + password_updated = account.updated; + }; +end + function provider.user_exists(username) local account = accounts:get(username); if not account then @@ -67,11 +77,18 @@ end function provider.create_user(username, password) + local now = os.time(); + if password == nil then + return accounts:set(username, { created = now, updated = now, disabled = true }); + end password = saslprep(password); if not password then return nil, "Password fails SASLprep."; end - return accounts:set(username, {password = password}); + return accounts:set(username, { + password = password; + created = now, updated = now; + }); end function provider.delete_user(username) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_auth_ldap.lua --- a/plugins/mod_auth_ldap.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_auth_ldap.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,6 @@ -- mod_auth_ldap -local jid_split = require "util.jid".split; -local new_sasl = require "util.sasl".new; +local new_sasl = require "prosody.util.sasl".new; local lualdap = require "lualdap"; local function ldap_filter_escape(s) @@ -13,14 +12,21 @@ local ldap_rootdn = module:get_option_string("ldap_rootdn", ""); local ldap_password = module:get_option_string("ldap_password", ""); local ldap_tls = module:get_option_boolean("ldap_tls"); -local ldap_scope = module:get_option_string("ldap_scope", "subtree"); +local ldap_scope = module:get_option_enum("ldap_scope", "subtree", "base", "onelevel"); local ldap_filter = module:get_option_string("ldap_filter", "(uid=$user)"):gsub("%%s", "$user", 1); local ldap_base = assert(module:get_option_string("ldap_base"), "ldap_base is a required option for ldap"); -local ldap_mode = module:get_option_string("ldap_mode", "bind"); +local ldap_mode = module:get_option_enum("ldap_mode", "bind", "getpasswd"); local ldap_admins = module:get_option_string("ldap_admin_filter", module:get_option_string("ldap_admins")); -- COMPAT with mistake in documentation local host = ldap_filter_escape(module:get_option_string("realm", module.host)); +if ldap_admins then + module:log("error", "The 'ldap_admin_filter' option has been deprecated, ".. + "and will be ignored. Equivalent functionality may be added in ".. + "the future if there is demand." + ); +end + -- Initiate connection local ld = nil; module.unload = function() if ld then pcall(ld, ld.close); end end @@ -133,22 +139,4 @@ module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode)); end -if ldap_admins then - function provider.is_admin(jid) - local username, user_host = jid_split(jid); - if user_host ~= module.host then - return false; - end - return ldap_do("search", 2, { - base = ldap_base; - scope = ldap_scope; - sizelimit = 1; - filter = ldap_admins:gsub("%$(%a+)", { - user = ldap_filter_escape(username); - host = host; - }); - }); - end -end - module:provides("auth", provider); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_authz_internal.lua --- a/plugins/mod_authz_internal.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_authz_internal.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,59 +1,350 @@ -local array = require "util.array"; -local it = require "util.iterators"; -local set = require "util.set"; -local jid_split = require "util.jid".split; -local normalize = require "util.jid".prep; +local array = require "prosody.util.array"; +local it = require "prosody.util.iterators"; +local set = require "prosody.util.set"; +local jid_split, jid_bare, jid_host = import("prosody.util.jid", "split", "bare", "host"); +local normalize = require "prosody.util.jid".prep; +local roles = require "prosody.util.roles"; + +local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize; local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize; local host = module.host; -local role_store = module:open_store("roles"); -local role_map_store = module:open_store("roles", "map"); +local host_suffix = host:gsub("^[^%.]+%.", ""); + +local hosts = prosody.hosts; +local is_anon_host = module:get_option_string("authentication") == "anonymous"; +local default_user_role = module:get_option_string("default_user_role", is_anon_host and "prosody:guest" or "prosody:registered"); + +local is_component = hosts[host].type == "component"; +local host_user_role, server_user_role, public_user_role; +if is_component then + host_user_role = module:get_option_string("host_user_role", "prosody:registered"); + server_user_role = module:get_option_string("server_user_role"); + public_user_role = module:get_option_string("public_user_role"); +end + +local role_store = module:open_store("account_roles"); +local role_map_store = module:open_store("account_roles", "map"); + +local role_registry = {}; -local admin_role = { ["prosody:admin"] = true }; +function register_role(role) + if role_registry[role.name] ~= nil then + return error("A role '"..role.name.."' is already registered"); + end + if not roles.is_role(role) then + -- Convert table syntax to real role object + for i, inherited_role in ipairs(role.inherits or {}) do + if type(inherited_role) == "string" then + role.inherits[i] = assert(role_registry[inherited_role], "The named role '"..inherited_role.."' is not registered"); + end + end + if not role.permissions then role.permissions = {}; end + for _, allow_permission in ipairs(role.allow or {}) do + role.permissions[allow_permission] = true; + end + for _, deny_permission in ipairs(role.deny or {}) do + role.permissions[deny_permission] = false; + end + role = roles.new(role); + end + role_registry[role.name] = role; +end + +-- Default roles + +-- For untrusted guest/anonymous users +register_role { + name = "prosody:guest"; + priority = 15; +}; -function get_user_roles(user) - if config_admin_jids:contains(user.."@"..host) then - return admin_role; +-- For e.g. self-registered accounts +register_role { + name = "prosody:registered"; + priority = 25; + inherits = { "prosody:guest" }; +}; + + +-- For trusted/provisioned accounts +register_role { + name = "prosody:member"; + priority = 35; + inherits = { "prosody:registered" }; +}; + +-- For administrators, e.g. of a host +register_role { + name = "prosody:admin"; + priority = 50; + inherits = { "prosody:member" }; +}; + +-- For server operators (full access) +register_role { + name = "prosody:operator"; + priority = 75; + inherits = { "prosody:admin" }; +}; + + +-- Process custom roles from config + +local custom_roles = module:get_option_array("custom_roles", {}); +for n, role_config in ipairs(custom_roles) do + local ok, err = pcall(register_role, role_config); + if not ok then + module:log("error", "Error registering custom role %s: %s", role_config.name or tostring(n), err); end - return role_store:get(user); +end + +-- Process custom permissions from config + +local config_add_perms = module:get_option("add_permissions", {}); +local config_remove_perms = module:get_option("remove_permissions", {}); + +for role_name, added_permissions in pairs(config_add_perms) do + if not role_registry[role_name] then + module:log("error", "Cannot add permissions to unknown role '%s'", role_name); + else + for _, permission in ipairs(added_permissions) do + role_registry[role_name]:set_permission(permission, true, true); + end + end end -function set_user_roles(user, roles) - role_store:set(user, roles) - return true; +for role_name, removed_permissions in pairs(config_remove_perms) do + if not role_registry[role_name] then + module:log("error", "Cannot remove permissions from unknown role '%s'", role_name); + else + for _, permission in ipairs(removed_permissions) do + role_registry[role_name]:set_permission(permission, false, true); + end + end +end + +-- Public API + +-- Get the primary role of a user +function get_user_role(user) + local bare_jid = user.."@"..host; + + -- Check config first + if config_global_admin_jids:contains(bare_jid) then + return role_registry["prosody:operator"]; + elseif config_admin_jids:contains(bare_jid) then + return role_registry["prosody:admin"]; + end + + -- Check storage + local stored_roles, err = role_store:get(user); + if not stored_roles then + if err then + -- Unable to fetch role, fail + return nil, err; + end + -- No role set, use default role + return role_registry[default_user_role]; + end + if stored_roles._default == nil then + -- No primary role explicitly set, return default + return role_registry[default_user_role]; + end + local primary_stored_role = role_registry[stored_roles._default]; + if not primary_stored_role then + return nil, "unknown-role"; + end + return primary_stored_role; end -function get_users_with_role(role) - local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role) or {})); - if role == "prosody:admin" then - local config_admin_users = config_admin_jids / function (admin_jid) +-- Set the primary role of a user +function set_user_role(user, role_name) + local role = role_registry[role_name]; + if not role then + return error("Cannot assign default user an unknown role: "..tostring(role_name)); + end + local keys_update = { + _default = role_name; + -- Primary role cannot be secondary role + [role_name] = role_map_store.remove; + }; + if role_name == default_user_role then + -- Don't store default + keys_update._default = role_map_store.remove; + end + local ok, err = role_map_store:set_keys(user, keys_update); + if not ok then + return nil, err; + end + return role; +end + +function add_user_secondary_role(user, role_name) + if not role_registry[role_name] then + return error("Cannot assign default user an unknown role: "..tostring(role_name)); + end + role_map_store:set(user, role_name, true); +end + +function remove_user_secondary_role(user, role_name) + role_map_store:set(user, role_name, nil); +end + +function get_user_secondary_roles(user) + local stored_roles, err = role_store:get(user); + if not stored_roles then + if err then + -- Unable to fetch role, fail + return nil, err; + end + -- No role set + return {}; + end + stored_roles._default = nil; + for role_name in pairs(stored_roles) do + stored_roles[role_name] = role_registry[role_name]; + end + return stored_roles; +end + +function user_can_assume_role(user, role_name) + local primary_role = get_user_role(user); + if primary_role and primary_role.name == role_name then + return true; + end + local secondary_roles = get_user_secondary_roles(user); + if secondary_roles and secondary_roles[role_name] then + return true; + end + return false; +end + +-- This function is *expensive* +function get_users_with_role(role_name) + local function role_filter(username, default_role) --luacheck: ignore 212/username + return default_role == role_name; + end + local primary_role_users = set.new(it.to_array(it.filter(role_filter, pairs(role_map_store:get_all("_default") or {})))); + local secondary_role_users = set.new(it.to_array(it.keys(role_map_store:get_all(role_name) or {}))); + + local config_set; + if role_name == "prosody:admin" then + config_set = config_admin_jids; + elseif role_name == "prosody:operator" then + config_set = config_global_admin_jids; + end + if config_set then + local config_admin_users = config_set / function (admin_jid) local j_node, j_host = jid_split(admin_jid); if j_host == host then return j_node; end end; - return it.to_array(config_admin_users + set.new(storage_role_users)); + return it.to_array(config_admin_users + primary_role_users + secondary_role_users); end - return storage_role_users; + return it.to_array(primary_role_users + secondary_role_users); end -function get_jid_roles(jid) - if config_admin_jids:contains(jid) then - return admin_role; +function get_jid_role(jid) + local bare_jid = jid_bare(jid); + if config_global_admin_jids:contains(bare_jid) then + return role_registry["prosody:operator"]; + elseif config_admin_jids:contains(bare_jid) then + return role_registry["prosody:admin"]; + elseif is_component then + local user_host = jid_host(bare_jid); + if host_user_role and user_host == host_suffix then + return role_registry[host_user_role]; + elseif server_user_role and hosts[user_host] then + return role_registry[server_user_role]; + elseif public_user_role then + return role_registry[public_user_role]; + end end return nil; end -function set_jid_roles(jid) -- luacheck: ignore 212 +function set_jid_role(jid, role_name) -- luacheck: ignore 212 return false; end -function get_jids_with_role(role) +function get_jids_with_role(role_name) -- Fetch role users from storage - local storage_role_jids = array.map(get_users_with_role(role), function (username) + local storage_role_jids = array.map(get_users_with_role(role_name), function (username) return username.."@"..host; end); - if role == "prosody:admin" then + if role_name == "prosody:admin" then return it.to_array(config_admin_jids + set.new(storage_role_jids)); + elseif role_name == "prosody:operator" then + return it.to_array(config_global_admin_jids + set.new(storage_role_jids)); end return storage_role_jids; end + +function add_default_permission(role_name, action, policy) + local role = role_registry[role_name]; + if not role then + module:log("warn", "Attempt to add default permission for unknown role: %s", role_name); + return nil, "no-such-role"; + end + if policy == nil then policy = true; end + module:log("debug", "Adding policy %s for permission %s on role %s", policy, action, role_name); + return role:set_permission(action, policy); +end + +function get_role_by_name(role_name) + return assert(role_registry[role_name], role_name); +end + +function get_all_roles() + return role_registry; +end + +-- COMPAT: Migrate from 0.12 role storage +local function do_migration(migrate_host) + local old_role_store = assert(module:context(migrate_host):open_store("roles")); + local new_role_store = assert(module:context(migrate_host):open_store("account_roles")); + + local migrated, failed, skipped = 0, 0, 0; + -- Iterate all users + for username in assert(old_role_store:users()) do + local old_roles = it.to_array(it.filter(function (k) return k:sub(1,1) ~= "_"; end, it.keys(old_role_store:get(username)))); + if #old_roles == 1 then + local ok, err = new_role_store:set(username, { + _default = old_roles[1]; + }); + if ok then + migrated = migrated + 1; + else + failed = failed + 1; + print("EE: Failed to store new role info for '"..username.."': "..err); + end + else + print("WW: User '"..username.."' has multiple roles and cannot be automatically migrated"); + skipped = skipped + 1; + end + end + return migrated, failed, skipped; +end + +function module.command(arg) + if arg[1] == "migrate" then + table.remove(arg, 1); + local migrate_host = arg[1]; + if not migrate_host or not prosody.hosts[migrate_host] then + print("EE: Please supply a valid host to migrate to the new role storage"); + return 1; + end + + -- Initialize storage layer + require "prosody.core.storagemanager".initialize_host(migrate_host); + + print("II: Migrating roles..."); + local migrated, failed, skipped = do_migration(migrate_host); + print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped)); + return (failed + skipped == 0) and 0 or 1; + else + print("EE: Unknown command: "..(arg[1] or "")); + print(" Hint: try 'migrate'?"); + end +end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_blocklist.lua --- a/plugins/mod_blocklist.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_blocklist.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,34 +9,29 @@ -- This module implements XEP-0191: Blocking Command -- -local user_exists = require"core.usermanager".user_exists; -local rostermanager = require"core.rostermanager"; +local user_exists = require"prosody.core.usermanager".user_exists; +local rostermanager = require"prosody.core.rostermanager"; local is_contact_subscribed = rostermanager.is_contact_subscribed; local is_contact_pending_in = rostermanager.is_contact_pending_in; local load_roster = rostermanager.load_roster; local save_roster = rostermanager.save_roster; -local st = require"util.stanza"; +local st = require"prosody.util.stanza"; local st_error_reply = st.error_reply; -local jid_prep = require"util.jid".prep; -local jid_split = require"util.jid".split; +local jid_prep = require"prosody.util.jid".prep; +local jid_split = require"prosody.util.jid".split; local storage = module:open_store(); local sessions = prosody.hosts[module.host].sessions; local full_sessions = prosody.full_sessions; --- First level cache of blocklists by username. --- Weak table so may randomly expire at any time. -local cache = setmetatable({}, { __mode = "v" }); - --- Second level of caching, keeps a fixed number of items, also anchors --- items in the above cache. +-- Cache of blocklists, keeps a fixed number of items. -- -- The size of this affects how often we will need to load a blocklist from -- disk, which we want to avoid during routing. On the other hand, we don't -- want to use too much memory either, so this can be tuned by advanced -- users. TODO use science to figure out a better default, 64 is just a guess. -local cache_size = module:get_option_number("blocklist_cache_size", 64); -local cache2 = require"util.cache".new(cache_size); +local cache_size = module:get_option_integer("blocklist_cache_size", 256, 1); +local blocklist_cache = require"prosody.util.cache".new(cache_size); local null_blocklist = {}; @@ -48,12 +43,12 @@ return ok, err; end -- Successful save, update the cache - cache2:set(username, blocklist); - cache[username] = blocklist; + blocklist_cache:set(username, blocklist); return true; end -- Migrates from the old mod_privacy storage +-- TODO mod_privacy was removed in 0.10.0, this should be phased out local function migrate_privacy_list(username) local legacy_data = module:open_store("privacy"):get(username); if not legacy_data or not legacy_data.lists or not legacy_data.default then return; end @@ -77,8 +72,15 @@ return migrated_data; end +if not module:get_option_boolean("migrate_legacy_blocking", true) then + migrate_privacy_list = function (username) + module:log("debug", "Migrating from mod_privacy disabled, user '%s' will start with a fresh blocklist", username); + return nil; + end +end + local function get_blocklist(username) - local blocklist = cache2:get(username); + local blocklist = blocklist_cache:get(username); if not blocklist then if not user_exists(username, module.host) then return null_blocklist; @@ -90,9 +92,8 @@ if not blocklist then blocklist = { [false] = { created = os.time(); }; }; end - cache2:set(username, blocklist); + blocklist_cache:set(username, blocklist); end - cache[username] = blocklist; return blocklist; end @@ -100,7 +101,7 @@ local origin, stanza = event.origin, event.stanza; local username = origin.username; local reply = st.reply(stanza):tag("blocklist", { xmlns = "urn:xmpp:blocking" }); - local blocklist = cache[username] or get_blocklist(username); + local blocklist = get_blocklist(username); for jid in pairs(blocklist) do if jid then reply:tag("item", { jid = jid }):up(); @@ -159,7 +160,7 @@ return true; end - local blocklist = cache[username] or get_blocklist(username); + local blocklist = get_blocklist(username); local new_blocklist = { -- We set the [false] key to something as a signal not to migrate privacy lists @@ -233,8 +234,7 @@ -- Cache invalidation, solved! module:hook_global("user-deleted", function (event) if event.host == module.host then - cache2:set(event.username, nil); - cache[event.username] = nil; + blocklist_cache:set(event.username, nil); end end); @@ -249,7 +249,7 @@ end); local function is_blocked(user, jid) - local blocklist = cache[user] or get_blocklist(user); + local blocklist = get_blocklist(user); if blocklist[jid] then return true; end local node, host = jid_split(jid); return blocklist[host] or node and blocklist[node..'@'..host]; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_bookmarks.lua --- a/plugins/mod_bookmarks.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_bookmarks.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,10 @@ -local mm = require "core.modulemanager"; +local mm = require "prosody.core.modulemanager"; if mm.get_modules_for_host(module.host):contains("bookmarks2") then error("mod_bookmarks and mod_bookmarks2 are conflicting, please disable one of them.", 0); end -local st = require "util.stanza"; -local jid_split = require "util.jid".split; +local st = require "prosody.util.stanza"; +local jid_split = require "prosody.util.jid".split; local mod_pep = module:depends "pep"; local private_storage = module:open_store("private", "map"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_bosh.lua --- a/plugins/mod_bosh.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_bosh.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,21 +8,21 @@ module:set_global(); -local new_xmpp_stream = require "util.xmppstream".new; -local sm = require "core.sessionmanager"; +local new_xmpp_stream = require "prosody.util.xmppstream".new; +local sm = require "prosody.core.sessionmanager"; local sm_destroy_session = sm.destroy_session; -local new_uuid = require "util.uuid".generate; +local new_uuid = require "prosody.util.uuid".generate; local core_process_stanza = prosody.core_process_stanza; -local st = require "util.stanza"; -local logger = require "util.logger"; +local st = require "prosody.util.stanza"; +local logger = require "prosody.util.logger"; local log = module._log; -local initialize_filters = require "util.filters".initialize; +local initialize_filters = require "prosody.util.filters".initialize; local math_min = math.min; local tostring, type = tostring, type; local traceback = debug.traceback; -local runner = require"util.async".runner; -local nameprep = require "util.encodings".stringprep.nameprep; -local cache = require "util.cache"; +local runner = require"prosody.util.async".runner; +local nameprep = require "prosody.util.encodings".stringprep.nameprep; +local cache = require "prosody.util.cache"; local xmlns_streams = "http://etherx.jabber.org/streams"; local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; @@ -36,16 +36,16 @@ local BOSH_MAX_REQUESTS = 2; -- The number of seconds a BOSH session should remain open with no requests -local bosh_max_inactivity = module:get_option_number("bosh_max_inactivity", 60); +local bosh_max_inactivity = module:get_option_period("bosh_max_inactivity", 60); -- The minimum amount of time between requests with no payload -local bosh_max_polling = module:get_option_number("bosh_max_polling", 5); +local bosh_max_polling = module:get_option_period("bosh_max_polling", 5); -- The maximum amount of time that the server will hold onto a request before replying -- (the client can set this to a lower value when it connects, if it chooses) -local bosh_max_wait = module:get_option_number("bosh_max_wait", 120); +local bosh_max_wait = module:get_option_period("bosh_max_wait", 120); 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_number("c2s_stanza_size_limit", 1024*256); +local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256, 10000); if cross_domain ~= nil then module:log("info", "The 'cross_domain_bosh' option has been deprecated"); @@ -325,7 +325,7 @@ sid = new_uuid(); -- TODO use util.session local session = { - type = "c2s_unauthed", conn = request.conn, sid = sid, host = attr.to, + base_type = "c2s", type = "c2s_unauthed", conn = request.conn, sid = sid, host = attr.to, rid = rid - 1, -- Hack for initial session setup, "previous" rid was $current_request - 1 bosh_version = attr.ver, bosh_wait = wait, streamid = sid, bosh_max_inactive = bosh_max_inactivity, bosh_responses = cache.new(BOSH_HOLD+1):table(); @@ -456,7 +456,7 @@ if session.notopen then local features = st.stanza("stream:features"); - module:context(session.host):fire_event("stream-features", { origin = session, features = features }); + module:context(session.host):fire_event("stream-features", { origin = session, features = features, stream = attr }); session.send(features); session.notopen = nil; end @@ -559,6 +559,6 @@ }); end -if require"core.modulemanager".get_modules_for_host("*"):contains(module.name) then +if require"prosody.core.modulemanager".get_modules_for_host("*"):contains(module.name) then module:add_host(); end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_c2s.lua --- a/plugins/mod_c2s.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_c2s.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,15 +8,15 @@ module:set_global(); -local add_task = require "util.timer".add_task; -local new_xmpp_stream = require "util.xmppstream".new; -local nameprep = require "util.encodings".stringprep.nameprep; -local sessionmanager = require "core.sessionmanager"; -local statsmanager = require "core.statsmanager"; -local st = require "util.stanza"; +local add_task = require "prosody.util.timer".add_task; +local new_xmpp_stream = require "prosody.util.xmppstream".new; +local nameprep = require "prosody.util.encodings".stringprep.nameprep; +local sessionmanager = require "prosody.core.sessionmanager"; +local statsmanager = require "prosody.core.statsmanager"; +local st = require "prosody.util.stanza"; local sm_new_session, sm_destroy_session = sessionmanager.new_session, sessionmanager.destroy_session; -local uuid_generate = require "util.uuid".generate; -local async = require "util.async"; +local uuid_generate = require "prosody.util.uuid".generate; +local async = require "prosody.util.async"; local runner = async.runner; local tostring, type = tostring, type; @@ -25,10 +25,10 @@ local log = module._log; -local c2s_timeout = module:get_option_number("c2s_timeout", 300); -local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); +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_number("c2s_stanza_size_limit", 1024*256); +local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024*256,10000); local measure_connections = module:metric("gauge", "connections", "", "Established c2s connections", {"host", "type", "ip_family"}); @@ -117,8 +117,7 @@ session.secure = true; session.encrypted = true; - local sock = session.conn:socket(); - local info = sock.info and sock:info(); + local info = session.conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; @@ -129,8 +128,13 @@ end local features = st.stanza("stream:features"); - hosts[session.host].events.fire_event("stream-features", { origin = session, features = 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 then + features:reset(); + features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }) + :text_tag("max-bytes", string.format("%d", stanza_size_limit)):up(); + end send(features); else if session.secure then @@ -248,6 +252,9 @@ local function disconnect_user_sessions(reason, leave_resource) return function (event) local username, host, resource = event.username, event.host, event.resource; + if not (hosts[host] and hosts[host].type == "local") then + return -- not a local VirtualHost so no sessions + end local user = hosts[host].sessions[username]; if user and user.sessions then for r, session in pairs(user.sessions) do @@ -260,8 +267,18 @@ end module:hook_global("user-password-changed", disconnect_user_sessions({ condition = "reset", text = "Password changed" }, true), 200); -module:hook_global("user-roles-changed", disconnect_user_sessions({ condition = "reset", text = "Roles changed" }), 200); +module:hook_global("user-role-changed", disconnect_user_sessions({ condition = "reset", text = "Role changed" }), 200); module:hook_global("user-deleted", disconnect_user_sessions({ condition = "not-authorized", text = "Account deleted" }), 200); +module:hook_global("user-disabled", disconnect_user_sessions({ condition = "not-authorized", text = "Account disabled" }), 200); + +module:hook_global("c2s-session-updated", function (event) + sessions[event.session.conn] = event.session; + local replaced_conn = event.replaced_conn; + if replaced_conn then + sessions[replaced_conn] = nil; + replaced_conn:close(); + end +end); function runner_callbacks:ready() if self.data.conn then @@ -293,10 +310,10 @@ if conn:ssl() then session.secure = true; session.encrypted = true; + session.ssl_ctx = conn:sslctx(); -- Check if TLS compression is used - local sock = conn:socket(); - local info = sock.info and sock:info(); + local info = conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; @@ -354,11 +371,13 @@ end end - if c2s_timeout then - add_task(c2s_timeout, function () + if c2s_timeout < math.huge then + session.c2s_timeout = add_task(c2s_timeout, function () if session.type == "c2s_unauthed" then (session.log or log)("debug", "Connection still not authenticated after c2s_timeout=%gs, closing it", c2s_timeout); session:close("connection-timeout"); + else + session.c2s_timeout = nil; end end); end @@ -426,7 +445,7 @@ module:hook("server-stopping", function(event) -- luacheck: ignore 212/event -- Close ports - local pm = require "core.portmanager"; + local pm = require "prosody.core.portmanager"; for _, netservice in pairs(module.items["net-provider"]) do pm.unregister_service(netservice.name, netservice); end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_carbons.lua --- a/plugins/mod_carbons.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_carbons.lua Wed Mar 27 15:39:03 2024 +0000 @@ -3,9 +3,9 @@ -- -- This file is MIT/X11 licensed. -local st = require "util.stanza"; -local jid_bare = require "util.jid".bare; -local jid_resource = require "util.jid".resource; +local st = require "prosody.util.stanza"; +local jid_bare = require "prosody.util.jid".bare; +local jid_resource = require "prosody.util.jid".resource; local xmlns_carbons = "urn:xmpp:carbons:2"; local xmlns_forward = "urn:xmpp:forward:0"; local full_sessions, bare_sessions = prosody.full_sessions, prosody.bare_sessions; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_component.lua --- a/plugins/mod_component.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_component.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,16 +10,16 @@ local t_concat = table.concat; local tostring, type = tostring, type; -local xpcall = require "util.xpcall".xpcall; +local xpcall = require "prosody.util.xpcall".xpcall; local traceback = debug.traceback; -local logger = require "util.logger"; -local sha1 = require "util.hashes".sha1; -local st = require "util.stanza"; +local logger = require "prosody.util.logger"; +local sha1 = require "prosody.util.hashes".sha1; +local st = require "prosody.util.stanza"; -local jid_split = require "util.jid".split; -local new_xmpp_stream = require "util.xmppstream".new; -local uuid_gen = require "util.uuid".generate; +local jid_host = require "prosody.util.jid".host; +local new_xmpp_stream = require "prosody.util.xmppstream".new; +local uuid_gen = require "prosody.util.uuid".generate; local core_process_stanza = prosody.core_process_stanza; local hosts = prosody.hosts; @@ -27,7 +27,8 @@ local log = module._log; local opt_keepalives = module:get_option_boolean("component_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); -local stanza_size_limit = module:get_option_number("component_stanza_size_limit", module:get_option_number("s2s_stanza_size_limit", 1024*512)); +local stanza_size_limit = module:get_option_integer("component_stanza_size_limit", + module:get_option_integer("s2s_stanza_size_limit", 1024 * 512, 10000), 10000); local sessions = module:shared("sessions"); @@ -85,7 +86,7 @@ end if env.connected then - local policy = module:get_option_string("component_conflict_resolve", "kick_new"); + local policy = module:get_option_enum("component_conflict_resolve", "kick_new", "kick_old"); if policy == "kick_old" then env.session:close{ condition = "conflict", text = "Replaced by a new connection" }; else -- kick_new @@ -222,22 +223,19 @@ end if not stanza.attr.xmlns or stanza.attr.xmlns == "jabber:client" then local from = stanza.attr.from; - if from then - if session.component_validate_from then - local _, domain = jid_split(stanza.attr.from); - if domain ~= session.host then - -- Return error - session.log("warn", "Component sent stanza with missing or invalid 'from' address"); - session:close{ - condition = "invalid-from"; - text = "Component tried to send from address <"..tostring(from) - .."> which is not in domain <"..tostring(session.host)..">"; - }; - return; - end + if session.component_validate_from then + if not from or (jid_host(from) ~= session.host) then + -- Return error + session.log("warn", "Component sent stanza with missing or invalid 'from' address"); + session:close{ + condition = "invalid-from"; + text = "Component tried to send from address <"..(from or "< [missing 'from' attribute] >") + .."> which is not in domain <"..tostring(session.host)..">"; + }; + return; end - else - stanza.attr.from = session.host; -- COMPAT: Strictly we shouldn't allow this + elseif not from then + stanza.attr.from = session.host; end if not stanza.attr.to then session.log("warn", "Rejecting stanza with no 'to' address"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_cron.lua --- a/plugins/mod_cron.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_cron.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,9 +1,10 @@ module:set_global(); -local async = require("util.async"); -local datetime = require("util.datetime"); +local async = require("prosody.util.async"); -local periods = { hourly = 3600; daily = 86400; weekly = 7 * 86400 } +local cron_initial_delay = module:get_option_number("cron_initial_delay", 1); +local cron_check_delay = module:get_option_number("cron_check_delay", 3600); +local cron_spread_factor = module:get_option_number("cron_spread_factor", 0); local active_hosts = {} @@ -14,18 +15,16 @@ local function save_task(task, started_at) last_run_times:set(nil, task.id, started_at); end + local function restore_task(task) if task.last == nil then task.last = last_run_times:get(nil, task.id); end end + local function task_added(event) local task = event.item; if task.name == nil then task.name = task.when; end if task.id == nil then task.id = event.source.name .. "/" .. task.name:gsub("%W", "_"):lower(); end - if task.last == nil then task.last = last_run_times:get(nil, task.id); end + task.period = host_module:get_option_period(task.id:gsub("/", "_") .. "_period", "1" .. task.when, 60, 86400 * 7 * 53); + task.restore = restore_task; task.save = save_task; - module:log("debug", "%s task %s added, last run %s", task.when, task.id, - task.last and datetime.datetime(task.last) or "never"); - if task.last == nil then - local now = os.time(); - task.last = now - now % periods[task.when]; - end + module:log("debug", "%s task %s added", task.when, task.id); return true end @@ -40,26 +39,55 @@ function host_module.unload() active_hosts[host_module.host] = nil; end end -local function should_run(when, last) return not last or last + periods[when] * 0.995 <= os.time() end +local function should_run(task, last) return not last or last + task.period * 0.995 <= os.time() end local function run_task(task) + task:restore(); + if not should_run(task, task.last) then return end local started_at = os.time(); task:run(started_at); task.last = started_at; task:save(started_at); end +local function spread(t, factor) + return t * (1 - factor + 2*factor*math.random()); +end + local task_runner = async.runner(run_task); -scheduled = module:add_timer(1, function() +scheduled = module:add_timer(cron_initial_delay, function() module:log("info", "Running periodic tasks"); - local delay = 3600; + local delay = spread(cron_check_delay, cron_spread_factor); for host in pairs(active_hosts) do module:log("debug", "Running periodic tasks for host %s", host); - for _, task in ipairs(module:context(host):get_host_items("task")) do - module:log("debug", "Considering %s task %s (%s)", task.when, task.id, task.run); - if should_run(task.when, task.last) then task_runner:run(task); end - end + for _, task in ipairs(module:context(host):get_host_items("task")) do task_runner:run(task); end end - module:log("debug", "Wait %ds", delay); + module:log("debug", "Wait %gs", delay); return delay end); + +module:add_item("shell-command", { + section = "cron"; + section_desc = "View and manage recurring tasks"; + name = "tasks"; + desc = "View registered tasks"; + args = {}; + handler = function(self, filter_host) + local format_table = require("prosody.util.human.io").table; + local it = require("util.iterators"); + local row = format_table({ + { title = "Host"; width = "2p" }; + { title = "Task"; width = "3p" }; + { title = "Desc"; width = "3p" }; + { title = "When"; width = "1p" }; + { title = "Last run"; width = "20" }; + }, self.session.width); + local print = self.session.print; + print(row()); + for host in it.sorted_pairs(filter_host and { [filter_host] = true } or active_hosts) do + for _, task in ipairs(module:context(host):get_host_items("task")) do + print(row({ host; task.id; task.name; task.when; task.last and os.date("%Y-%m-%d %R:%S", task.last) or "never" })); + end + end + end; +}); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_csi.lua --- a/plugins/mod_csi.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_csi.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,11 @@ -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local xmlns_csi = "urn:xmpp:csi:0"; local csi_feature = st.stanza("csi", { xmlns = xmlns_csi }); -local csi_handler_available = nil; +local change = module:metric("counter", "changes", "events", "CSI state changes", {"csi_state"}); + module:hook("stream-features", function (event) - if event.origin.username and csi_handler_available then + if event.origin.username then event.features:add_child(csi_feature); end end); @@ -13,6 +14,7 @@ return function (event) if event.origin.username then event.origin.state = event.stanza.name; + change:with_labels(event.stanza.name):add(1); module:fire_event(name, event); return true; end @@ -21,15 +23,3 @@ module:hook("stanza/"..xmlns_csi..":active", refire_event("csi-client-active")); module:hook("stanza/"..xmlns_csi..":inactive", refire_event("csi-client-inactive")); - -function module.load() - if prosody.hosts[module.host].events._handlers["csi-client-active"] then - csi_handler_available = true; - module:set_status("core", "CSI handler module loaded"); - else - csi_handler_available = false; - module:set_status("warn", "No CSI handler module loaded"); - end -end -module:hook("module-loaded", module.load); -module:hook("module-unloaded", module.load); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_csi_simple.lua --- a/plugins/mod_csi_simple.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_csi_simple.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,14 +6,14 @@ module:depends"csi" -local jid = require "util.jid"; -local st = require "util.stanza"; -local dt = require "util.datetime"; -local filters = require "util.filters"; -local timer = require "util.timer"; +local jid = require "prosody.util.jid"; +local st = require "prosody.util.stanza"; +local dt = require "prosody.util.datetime"; +local filters = require "prosody.util.filters"; +local timer = require "prosody.util.timer"; -local queue_size = module:get_option_number("csi_queue_size", 256); -local resume_delay = module:get_option_number("csi_resume_inactive_delay", 5); +local queue_size = module:get_option_integer("csi_queue_size", 256, 1); +local resume_delay = module:get_option_period("csi_resume_inactive_delay", 5); local important_payloads = module:get_option_set("csi_important_payloads", { }); @@ -116,6 +116,9 @@ { "reason" } ); +local flush_sizes = module:metric("histogram", "flush_stanza_count", "", "Number of stanzas flushed at once", {}, + { buckets = { 0, 1, 2, 4, 8, 16, 32, 64, 128, 256 } }):with_labels(); + local function manage_buffer(stanza, session) local ctr = session.csi_counter or 0; if session.state ~= "inactive" then @@ -129,6 +132,7 @@ session.csi_measure_buffer_hold = nil; end flush_reasons:with_labels(why or "important"):add(1); + flush_sizes:sample(ctr); session.log("debug", "Flushing buffer (%s; queue size is %d)", why or "important", session.csi_counter); session.state = "flushing"; module:fire_event("csi-flushing", { session = session }); @@ -147,6 +151,7 @@ session.log("debug", "Flushing buffer (%s; queue size is %d)", "client activity", session.csi_counter); session.state = "flushing"; module:fire_event("csi-flushing", { session = session }); + flush_sizes:sample(ctr); flush_reasons:with_labels("client activity"):add(1); if session.csi_measure_buffer_hold then session.csi_measure_buffer_hold(); @@ -258,7 +263,7 @@ return 1; end -- luacheck: ignore 212/self - local xmppstream = require "util.xmppstream"; + local xmppstream = require "prosody.util.xmppstream"; local input_session = { notopen = true } local stream_callbacks = { stream_ns = "jabber:client", default_ns = "jabber:client" }; function stream_callbacks:handlestanza(stanza) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_debug_reset.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/mod_debug_reset.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,36 @@ +-- This module will "reset" the server when the client connection count drops +-- to zero. This is somewhere between a reload and a full process restart. +-- It is useful to ensure isolation between test runs, for example. It may +-- also be of use for some kinds of manual testing. + +module:set_global(); + +local hostmanager = require "prosody.core.hostmanager"; + +local function do_reset() + module:log("info", "Performing reset..."); + local hosts = {}; + for host in pairs(prosody.hosts) do + table.insert(hosts, host); + end + module:fire_event("server-resetting"); + for _, host in ipairs(hosts) do + hostmanager.deactivate(host); + hostmanager.activate(host); + module:log("info", "Reset complete"); + module:fire_event("server-reset"); + end +end + +function module.add_host(host_module) + host_module:hook("resource-unbind", function () + if next(prosody.full_sessions) == nil then + do_reset(); + end + end); +end + +local console_env = module:shared("/*/admin_shell/env"); +console_env.debug_reset = { + reset = do_reset; +}; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_debug_stanzas/watcher.lib.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/mod_debug_stanzas/watcher.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,220 @@ +local filters = require "prosody.util.filters"; +local jid = require "prosody.util.jid"; +local set = require "prosody.util.set"; + +local client_watchers = {}; + +-- active_filters[session] = { +-- filter_func = filter_func; +-- downstream = { cb1, cb2, ... }; +-- } +local active_filters = {}; + +local function subscribe_session_stanzas(session, handler, reason) + if active_filters[session] then + table.insert(active_filters[session].downstream, handler); + if reason then + handler(reason, nil, session); + end + return; + end + local downstream = { handler }; + active_filters[session] = { + filter_in = function (stanza) + module:log("debug", "NOTIFY WATCHER %d", #downstream); + for i = 1, #downstream do + downstream[i]("received", stanza, session); + end + return stanza; + end; + filter_out = function (stanza) + module:log("debug", "NOTIFY WATCHER %d", #downstream); + for i = 1, #downstream do + downstream[i]("sent", stanza, session); + end + return stanza; + end; + downstream = downstream; + }; + filters.add_filter(session, "stanzas/in", active_filters[session].filter_in); + filters.add_filter(session, "stanzas/out", active_filters[session].filter_out); + if reason then + handler(reason, nil, session); + end +end + +local function unsubscribe_session_stanzas(session, handler, reason) + local active_filter = active_filters[session]; + if not active_filter then + return; + end + for i = #active_filter.downstream, 1, -1 do + if active_filter.downstream[i] == handler then + table.remove(active_filter.downstream, i); + if reason then + handler(reason, nil, session); + end + end + end + if #active_filter.downstream == 0 then + filters.remove_filter(session, "stanzas/in", active_filter.filter_in); + filters.remove_filter(session, "stanzas/out", active_filter.filter_out); + end + active_filters[session] = nil; +end + +local function unsubscribe_all_from_session(session, reason) + local active_filter = active_filters[session]; + if not active_filter then + return; + end + for i = #active_filter.downstream, 1, -1 do + local handler = table.remove(active_filter.downstream, i); + if reason then + handler(reason, nil, session); + end + end + filters.remove_filter(session, "stanzas/in", active_filter.filter_in); + filters.remove_filter(session, "stanzas/out", active_filter.filter_out); + active_filters[session] = nil; +end + +local function unsubscribe_handler_from_all(handler, reason) + for session in pairs(active_filters) do + unsubscribe_session_stanzas(session, handler, reason); + end +end + +local s2s_watchers = {}; + +module:hook("s2sin-established", function (event) + for _, watcher in ipairs(s2s_watchers) do + if watcher.target_spec == event.session.from_host then + subscribe_session_stanzas(event.session, watcher.handler, "opened"); + end + end +end); + +module:hook("s2sout-established", function (event) + for _, watcher in ipairs(s2s_watchers) do + if watcher.target_spec == event.session.to_host then + subscribe_session_stanzas(event.session, watcher.handler, "opened"); + end + end +end); + +module:hook("s2s-closed", function (event) + unsubscribe_all_from_session(event.session, "closed"); +end); + +local watched_hosts = set.new(); + +local handler_map = setmetatable({}, { __mode = "kv" }); + +local function add_stanza_watcher(spec, orig_handler) + local function filtering_handler(event_type, stanza, session) + if stanza and spec.filter_spec then + if spec.filter_spec.with_jid then + if event_type == "sent" and (not stanza.attr.from or not jid.compare(stanza.attr.from, spec.filter_spec.with_jid)) then + return; + elseif event_type == "received" and (not stanza.attr.to or not jid.compare(stanza.attr.to, spec.filter_spec.with_jid)) then + return; + end + end + end + return orig_handler(event_type, stanza, session); + end + handler_map[orig_handler] = filtering_handler; + if spec.target_spec.jid then + local target_is_remote_host = not jid.node(spec.target_spec.jid) and not prosody.hosts[spec.target_spec.jid]; + + if target_is_remote_host then + -- Watch s2s sessions + table.insert(s2s_watchers, { + target_spec = spec.target_spec.jid; + handler = filtering_handler; + orig_handler = orig_handler; + }); + + -- Scan existing s2sin for matches + for session in pairs(prosody.incoming_s2s) do + if spec.target_spec.jid == session.from_host then + subscribe_session_stanzas(session, filtering_handler, "attached"); + end + end + -- Scan existing s2sout for matches + for local_host, local_session in pairs(prosody.hosts) do --luacheck: ignore 213/local_host + for remote_host, remote_session in pairs(local_session.s2sout) do + if spec.target_spec.jid == remote_host then + subscribe_session_stanzas(remote_session, filtering_handler, "attached"); + end + end + end + else + table.insert(client_watchers, { + target_spec = spec.target_spec.jid; + handler = filtering_handler; + orig_handler = orig_handler; + }); + local host = jid.host(spec.target_spec.jid); + if not watched_hosts:contains(host) and prosody.hosts[host] then + module:context(host):hook("resource-bind", function (event) + for _, watcher in ipairs(client_watchers) do + module:log("debug", "NEW CLIENT: %s vs %s", event.session.full_jid, watcher.target_spec); + if jid.compare(event.session.full_jid, watcher.target_spec) then + module:log("debug", "MATCH"); + subscribe_session_stanzas(event.session, watcher.handler, "opened"); + else + module:log("debug", "NO MATCH"); + end + end + end); + + module:context(host):hook("resource-unbind", function (event) + unsubscribe_all_from_session(event.session, "closed"); + end); + + watched_hosts:add(host); + end + for full_jid, session in pairs(prosody.full_sessions) do + if jid.compare(full_jid, spec.target_spec.jid) then + subscribe_session_stanzas(session, filtering_handler, "attached"); + end + end + end + else + error("No recognized target selector"); + end +end + +local function remove_stanza_watcher(orig_handler) + local handler = handler_map[orig_handler]; + unsubscribe_handler_from_all(handler, "detached"); + handler_map[orig_handler] = nil; + + for i = #client_watchers, 1, -1 do + if client_watchers[i].orig_handler == orig_handler then + table.remove(client_watchers, i); + end + end + + for i = #s2s_watchers, 1, -1 do + if s2s_watchers[i].orig_handler == orig_handler then + table.remove(s2s_watchers, i); + end + end +end + +local function cleanup(reason) + client_watchers = {}; + s2s_watchers = {}; + for session in pairs(active_filters) do + unsubscribe_all_from_session(session, reason or "cancelled"); + end +end + +return { + add = add_stanza_watcher; + remove = remove_stanza_watcher; + cleanup = cleanup; +}; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_dialback.lua --- a/plugins/mod_dialback.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_dialback.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,12 +10,12 @@ local log = module._log; -local st = require "util.stanza"; -local sha256_hash = require "util.hashes".sha256; -local sha256_hmac = require "util.hashes".hmac_sha256; -local secure_equals = require "util.hashes".equals; -local nameprep = require "util.encodings".stringprep.nameprep; -local uuid_gen = require"util.uuid".generate; +local st = require "prosody.util.stanza"; +local sha256_hash = require "prosody.util.hashes".sha256; +local sha256_hmac = require "prosody.util.hashes".hmac_sha256; +local secure_equals = require "prosody.util.hashes".equals; +local nameprep = require "prosody.util.encodings".stringprep.nameprep; +local uuid_gen = require"prosody.util.uuid".generate; local xmlns_stream = "http://etherx.jabber.org/streams"; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_disco.lua --- a/plugins/mod_disco.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_disco.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,13 +6,12 @@ -- COPYING file in the source package for more information. -- -local get_children = require "core.hostmanager".get_children; -local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local um_is_admin = require "core.usermanager".is_admin; -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local st = require "util.stanza" -local calculate_hash = require "util.caps".calculate_hash; +local get_children = require "prosody.core.hostmanager".get_children; +local is_contact_subscribed = require "prosody.core.rostermanager".is_contact_subscribed; +local jid_split = require "prosody.util.jid".split; +local jid_bare = require "prosody.util.jid".bare; +local st = require "prosody.util.stanza" +local calculate_hash = require "prosody.util.caps".calculate_hash; local expose_admins = module:get_option_boolean("disco_expose_admins", false); @@ -162,14 +161,16 @@ end end); +module:default_permission("prosody:admin", ":be-discovered-admin"); + -- Handle disco requests to user accounts if module:get_host_type() ~= "local" then return end -- skip for components module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(event) local origin, stanza = event.origin, event.stanza; local node = stanza.tags[1].attr.node; local username = jid_split(stanza.attr.to) or origin.username; - local is_admin = um_is_admin(stanza.attr.to or origin.full_jid, module.host) - if not stanza.attr.to or (expose_admins and is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + local target_is_admin = module:may(":be-discovered-admin", stanza.attr.to or origin.full_jid); + if not stanza.attr.to or (expose_admins and target_is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then if node and node ~= "" then local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); reply:tag("feature", { var = "http://jabber.org/protocol/disco#info" }):up(); @@ -187,7 +188,7 @@ end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - if is_admin then + if target_is_admin then reply:tag('identity', {category='account', type='admin'}):up(); elseif prosody.hosts[module.host].users.name == "anonymous" then reply:tag('identity', {category='account', type='anonymous'}):up(); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_external_services.lua --- a/plugins/mod_external_services.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_external_services.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,22 +1,22 @@ -local dt = require "util.datetime"; -local base64 = require "util.encodings".base64; -local hashes = require "util.hashes"; -local st = require "util.stanza"; -local jid = require "util.jid"; -local array = require "util.array"; -local set = require "util.set"; +local dt = require "prosody.util.datetime"; +local base64 = require "prosody.util.encodings".base64; +local hashes = require "prosody.util.hashes"; +local st = require "prosody.util.stanza"; +local jid = require "prosody.util.jid"; +local array = require "prosody.util.array"; +local set = require "prosody.util.set"; local default_host = module:get_option_string("external_service_host", module.host); -local default_port = module:get_option_number("external_service_port"); +local default_port = module:get_option_integer("external_service_port", nil, 1, 65535); local default_secret = module:get_option_string("external_service_secret"); -local default_ttl = module:get_option_number("external_service_ttl", 86400); +local default_ttl = module:get_option_period("external_service_ttl", "1 day"); local configured_services = module:get_option_array("external_services", {}); local access = module:get_option_set("external_service_access", {}); --- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 +-- https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00 local function behave_turn_rest_credentials(srv, item, secret) local ttl = default_ttl; if type(item.ttl) == "number" then diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_groups.lua --- a/plugins/mod_groups.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_groups.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,8 +10,8 @@ local groups; local members; -local datamanager = require "util.datamanager"; -local jid_prep = require "util.jid".prep; +local datamanager = require "prosody.util.datamanager"; +local jid_prep = require "prosody.util.jid".prep; local module_host = module:get_host(); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_http.lua --- a/plugins/mod_http.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_http.lua Wed Mar 27 15:39:03 2024 +0000 @@ -11,24 +11,26 @@ module:depends("http_errors"); end); -local portmanager = require "core.portmanager"; -local moduleapi = require "core.moduleapi"; +local portmanager = require "prosody.core.portmanager"; +local moduleapi = require "prosody.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 http_util = require "prosody.util.http"; +local normalize_path = http_util.normalize_path; +local set = require "prosody.util.set"; +local array = require "prosody.util.array"; -local ip_util = require "util.ip"; +local ip_util = require "prosody.util.ip"; local new_ip = ip_util.new_ip; local match_ip = ip_util.match; local parse_cidr = ip_util.parse_cidr; -local server = require "net.http.server"; +local server = require "prosody.net.http.server"; 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")); +server.set_option("body_size_limit", module:get_option_number("http_max_content_size", nil, 0)); +server.set_option("buffer_size_limit", module:get_option_number("http_max_buffer_size", nil, 0)); -- CORS settings local cors_overrides = module:get_option("http_cors_override", {}); @@ -36,7 +38,7 @@ local opt_headers = module:get_option_set("access_control_allow_headers", { "Content-Type" }); local opt_origins = module:get_option_set("access_control_allow_origins"); local opt_credentials = module:get_option_boolean("access_control_allow_credentials", false); -local opt_max_age = module:get_option_number("access_control_max_age", 2 * 60 * 60); +local opt_max_age = module:get_option_period("access_control_max_age", "2 hours"); local opt_default_cors = module:get_option_boolean("http_default_cors_enabled", true); local function get_http_event(host, app_path, key) @@ -75,11 +77,12 @@ local ports_by_scheme = { http = 80, https = 443, }; -- Helper to deduce a module's external URL -function moduleapi.http_url(module, app_name, default_path) +function moduleapi.http_url(module, app_name, default_path, mode) app_name = app_name or (module.name:gsub("^http_", "")); local external_url = url_parse(module:get_option_string("http_external_url")); - if external_url then + if external_url and mode ~= "internal" then + -- Current URL does not depend on knowing which ports are used, only configuration. local url = { scheme = external_url.scheme; host = external_url.host; @@ -91,6 +94,36 @@ return url_build(url); end + if prosody.process_type ~= "prosody" then + -- We generally don't open ports outside of Prosody, so we can't rely on + -- portmanager to tell us which ports and services are used and derive the + -- URL from that, so instead we derive it entirely from configuration. + local https_ports = module:get_option_array("https_ports", { 5281 }); + local scheme = "https"; + local port = tonumber(https_ports[1]); + if not port then + -- https is disabled and no http_external_url set + scheme = "http"; + local http_ports = module:get_option_array("http_ports", { 5280 }); + port = tonumber(http_ports[1]); + if not port then + return "http://disabled.invalid/"; + end + end + + local url = { + scheme = scheme; + host = module:get_option_string("http_host", module.global and module:get_option_string("http_default_host") or module.host); + port = port; + path = get_base_path(module, app_name, default_path or "/" .. app_name); + } + if ports_by_scheme[url.scheme] == url.port then + url.port = nil + end + return url_build(url); + end + + -- Use portmanager to find the actual port of https or http services local services = portmanager.get_active_services(); local http_services = services:get("https") or services:get("http") or {}; for interface, ports in pairs(http_services) do -- luacheck: ignore 213/interface @@ -112,12 +145,16 @@ return "http://disabled.invalid/"; end +local function header_set_tostring(header_value) + return array(header_value:items()):concat(", "); +end + local function apply_cors_headers(response, methods, headers, max_age, allow_credentials, allowed_origins, origin) if allowed_origins and not allowed_origins[origin] then return; end - response.headers.access_control_allow_methods = tostring(methods); - response.headers.access_control_allow_headers = tostring(headers); + response.headers.access_control_allow_methods = header_set_tostring(methods); + response.headers.access_control_allow_headers = header_set_tostring(headers); response.headers.access_control_max_age = tostring(max_age) response.headers.access_control_allow_origin = origin or "*"; if allow_credentials then @@ -292,7 +329,13 @@ local trusted_proxies = module:get_option_set("trusted_proxies", { "127.0.0.1", "::1" })._items; +--- deal with [ipv6]:port / ip:port format +local function normal_ip(ip) + return ip:match("^%[([%x:]*)%]") or ip:match("^([%d.]+)") or ip; +end + local function is_trusted_proxy(ip) + ip = normal_ip(ip); if trusted_proxies[ip] then return true; end @@ -308,6 +351,30 @@ local function get_forwarded_connection_info(request) --> ip:string, secure:boolean local ip = request.ip; local secure = request.secure; -- set by net.http.server + + local forwarded = http_util.parse_forwarded(request.headers.forwarded); + if forwarded then + request.forwarded = forwarded; + for i = #forwarded, 1, -1 do + local proxy = forwarded[i] + if is_trusted_proxy(ip) then + ip = normal_ip(proxy["for"]); + secure = secure and proxy.proto == "https"; + else + break + end + end + end + + return ip, secure; +end + +-- TODO switch to RFC 7239 by default once support is more common +if module:get_option_boolean("http_legacy_x_forwarded", true) then +function get_forwarded_connection_info(request) --> ip:string, secure:boolean + local ip = request.ip; + local secure = request.secure; -- set by net.http.server + local forwarded_for = request.headers.x_forwarded_for; if forwarded_for then -- luacheck: ignore 631 @@ -330,6 +397,7 @@ return ip, secure; end +end module:wrap_object_event(server._events, false, function (handlers, event_name, event_data) local request = event_data.request; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_http_errors.lua --- a/plugins/mod_http_errors.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_http_errors.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,9 +1,9 @@ module:set_global(); -local server = require "net.http.server"; -local codes = require "net.http.codes"; -local xml_escape = require "util.stanza".xml_escape; -local render = require "util.interpolation".new("%b{}", xml_escape); +local server = require "prosody.net.http.server"; +local codes = require "prosody.net.http.codes"; +local xml_escape = require "prosody.util.stanza".xml_escape; +local render = require "prosody.util.interpolation".new("%b{}", xml_escape); local show_private = module:get_option_boolean("http_errors_detailed", false); local always_serve = module:get_option_boolean("http_errors_always_show", true); @@ -35,13 +35,13 @@ {title} diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_http_file_share.lua --- a/plugins/mod_http_file_share.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_http_file_share.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,17 +8,16 @@ -- Again, from the top! local t_insert = table.insert; -local jid = require "util.jid"; -local st = require "util.stanza"; +local jid = require "prosody.util.jid"; +local st = require "prosody.util.stanza"; local url = require "socket.url"; -local dm = require "core.storagemanager".olddm; -local jwt = require "util.jwt"; -local errors = require "util.error"; -local dataform = require "util.dataforms".new; -local urlencode = require "util.http".urlencode; -local dt = require "util.datetime"; -local hi = require "util.human.units"; -local cache = require "util.cache"; +local dm = require "prosody.core.storagemanager".olddm; +local errors = require "prosody.util.error"; +local dataform = require "prosody.util.dataforms".new; +local urlencode = require "prosody.util.http".urlencode; +local dt = require "prosody.util.datetime"; +local hi = require "prosody.util.human.units"; +local cache = require "prosody.util.cache"; local lfs = require "lfs"; local unknown = math.abs(0/0); @@ -35,17 +34,21 @@ local persist_stats = module:open_store("upload_stats", "map"); -- id, , time, owner -local secret = module:get_option_string(module.name.."_secret", require"util.id".long()); +local secret = module:get_option_string(module.name.."_secret", require"prosody.util.id".long()); local external_base_url = module:get_option_string(module.name .. "_base_url"); -local file_size_limit = module:get_option_number(module.name .. "_size_limit", 10 * 1024 * 1024); -- 10 MB +local file_size_limit = module:get_option_integer(module.name .. "_size_limit", 10 * 1024 * 1024, 0); -- 10 MB local file_types = module:get_option_set(module.name .. "_allowed_file_types", {}); local safe_types = module:get_option_set(module.name .. "_safe_file_types", {"image/*","video/*","audio/*","text/plain"}); -local expiry = module:get_option_number(module.name .. "_expires_after", 7 * 86400); -local daily_quota = module:get_option_number(module.name .. "_daily_quota", file_size_limit*10); -- 100 MB / day -local total_storage_limit = module:get_option_number(module.name.."_global_quota", unlimited); +local expiry = module:get_option_period(module.name .. "_expires_after", "1w"); +local daily_quota = module:get_option_integer(module.name .. "_daily_quota", file_size_limit*10, 0); -- 100 MB / day +local total_storage_limit = module:get_option_integer(module.name.."_global_quota", unlimited, 0); + +local create_jwt, verify_jwt = require"prosody.util.jwt".init("HS256", secret, secret, { default_ttl = 600 }); local access = module:get_option_set(module.name .. "_access", {}); +module:default_permission("prosody:registered", ":upload"); + if not external_base_url then module:depends("http"); end @@ -76,12 +79,12 @@ local measure_quota_cache_size = module:measure("quota_cache", "amount"); local measure_total_storage_usage = module:measure("total_storage", "amount", { unit = "bytes" }); -do +module:on_ready(function () local total, err = persist_stats:get(nil, "total"); if not err then total_storage_usage = tonumber(total) or 0; end -end +end) module:hook_global("stats-update", function () measure_upload_cache_size(upload_cache:count()); @@ -135,7 +138,7 @@ function may_upload(uploader, filename, filesize, filetype) -- > boolean, error local uploader_host = jid.host(uploader); - if not ((access:empty() and prosody.hosts[uploader_host]) or access:contains(uploader) or access:contains(uploader_host)) then + if not (module:may(":upload", uploader) or access:contains(uploader) or access:contains(uploader_host)) then return false, upload_errors.new("access"); end @@ -169,16 +172,13 @@ end function get_authz(slot, uploader, filename, filesize, filetype) -local now = os.time(); - return jwt.sign(secret, { + return create_jwt({ -- token properties sub = uploader; - iat = now; - exp = now+300; -- slot properties slot = slot; - expires = expiry >= 0 and (now+expiry) or nil; + expires = expiry < math.huge and (os.time()+expiry) or nil; -- file properties filename = filename; filesize = filesize; @@ -249,32 +249,34 @@ function handle_upload(event, path) -- PUT /upload/:slot local request = event.request; - local authz = request.headers.authorization; - if authz then - authz = authz:match("^Bearer (.*)") - end - if not authz then - module:log("debug", "Missing or malformed Authorization header"); - event.response.headers.www_authenticate = "Bearer"; - return 401; - end - local authed, upload_info = jwt.verify(secret, authz); - if not (authed and type(upload_info) == "table" and type(upload_info.exp) == "number") then - module:log("debug", "Unauthorized or invalid token: %s, %q", authed, upload_info); - return 401; - end - if not request.body_sink and upload_info.exp < os.time() then - module:log("debug", "Authorization token expired on %s", dt.datetime(upload_info.exp)); - return 410; - end - if not path or upload_info.slot ~= path:match("^[^/]+") then - module:log("debug", "Invalid upload slot: %q, path: %q", upload_info.slot, path); - return 400; - end - if request.headers.content_length and tonumber(request.headers.content_length) ~= upload_info.filesize then - return 413; - -- Note: We don't know the size if the upload is streamed in chunked encoding, - -- so we also check the final file size on completion. + local upload_info = request.http_file_share_upload_info; + + if not upload_info then -- Initial handling of request + local authz = request.headers.authorization; + if authz then + authz = authz:match("^Bearer (.*)") + end + if not authz then + module:log("debug", "Missing or malformed Authorization header"); + event.response.headers.www_authenticate = "Bearer"; + return 401; + end + local authed, authed_upload_info = verify_jwt(authz); + if not authed then + module:log("debug", "Unauthorized or invalid token: %s, %q", authz, authed_upload_info); + return 401; + end + if not path or authed_upload_info.slot ~= path:match("^[^/]+") then + module:log("debug", "Invalid upload slot: %q, path: %q", authed_upload_info.slot, path); + return 400; + end + if request.headers.content_length and tonumber(request.headers.content_length) ~= authed_upload_info.filesize then + return 413; + -- Note: We don't know the size if the upload is streamed in chunked encoding, + -- so we also check the final file size on completion. + end + upload_info = authed_upload_info; + request.http_file_share_upload_info = upload_info; end local filename = get_filename(upload_info.slot, true); @@ -452,9 +454,9 @@ if expiry >= 0 and not external_base_url then -- TODO HTTP DELETE to the external endpoint? - local array = require "util.array"; - local async = require "util.async"; - local ENOENT = require "util.pposix".ENOENT; + local array = require "prosody.util.array"; + local async = require "prosody.util.async"; + local ENOENT = require "prosody.util.pposix".ENOENT; local function sleep(t) local wait, done = async.waiter(); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_http_files.lua --- a/plugins/mod_http_files.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_http_files.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,11 +9,11 @@ module:depends("http"); local open = io.open; -local fileserver = require"net.http.files"; +local fileserver = require"prosody.net.http.files"; local base_path = module:get_option_path("http_files_dir", module:get_option_path("http_path")); -local cache_size = module:get_option_number("http_files_cache_size", 128); -local cache_max_file_size = module:get_option_number("http_files_cache_max_file_size", 4096); +local cache_size = module:get_option_integer("http_files_cache_size", 128, 1); +local cache_max_file_size = module:get_option_integer("http_files_cache_max_file_size", 4096, 1); local dir_indices = module:get_option_array("http_index_files", { "index.html", "index.htm" }); local directory_index = module:get_option_boolean("http_dir_listing"); @@ -74,12 +74,12 @@ if opts.index_files == nil then opts.index_files = dir_indices; end - module:log("warn", "%s should be updated to use 'net.http.files' instead of mod_http_files", get_calling_module()); + module:log("warn", "%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module()); return fileserver.serve(opts); end function wrap_route(routes) - module:log("debug", "%s should be updated to use 'net.http.files' instead of mod_http_files", get_calling_module()); + module:log("debug", "%s should be updated to use 'prosody.net.http.files' instead of mod_http_files", get_calling_module()); for route,handler in pairs(routes) do if type(handler) ~= "function" then routes[route] = fileserver.serve(handler); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_http_openmetrics.lua --- a/plugins/mod_http_openmetrics.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_http_openmetrics.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,8 +8,8 @@ module:set_global(); -local statsman = require "core.statsmanager"; -local ip = require "util.ip"; +local statsman = require "prosody.core.statsmanager"; +local ip = require "prosody.util.ip"; local get_metric_registry = statsman.get_metric_registry; local collect = statsman.collect; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_invites.lua --- a/plugins/mod_invites.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_invites.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,12 @@ -local id = require "util.id"; -local it = require "util.iterators"; +local id = require "prosody.util.id"; +local it = require "prosody.util.iterators"; local url = require "socket.url"; -local jid_node = require "util.jid".node; -local jid_split = require "util.jid".split; +local jid_node = require "prosody.util.jid".node; +local jid_split = require "prosody.util.jid".split; +local argparse = require "prosody.util.argparse"; +local human_io = require "prosody.util.human.io"; -local default_ttl = module:get_option_number("invite_expiry", 86400 * 7); +local default_ttl = module:get_option_period("invite_expiry", "1 week"); local token_storage; if prosody.process_type == "prosody" or prosody.shutdown then @@ -201,53 +203,103 @@ end --- shell command -do - -- Since the console is global this overwrites the command for - -- each host it's loaded on, but this should be fine. - - local get_module = require "core.modulemanager".get_module; - - local console_env = module:shared("/*/admin_shell/env"); +module:add_item("shell-command", { + section = "invite"; + section_desc = "Create and manage invitations"; + name = "create_account"; + desc = "Create an invitation to make an account on this server with the specified JID (supply only a hostname to allow any username)"; + args = { { name = "user_jid", type = "string" } }; + host_selector = "user_jid"; - -- luacheck: ignore 212/self - console_env.invite = {}; - function console_env.invite:create_account(user_jid) - local username, host = jid_split(user_jid); - local mod_invites, err = get_module(host, "invites"); - if not mod_invites then return nil, err or "mod_invites not loaded on this host"; end - local invite, err = mod_invites.create_account(username); + handler = function (self, user_jid) --luacheck: ignore 212/self + local username = jid_split(user_jid); + local invite, err = create_account(username); if not invite then return nil, err; end return true, invite.landing_page or invite.uri; - end + end; +}); - function console_env.invite:create_contact(user_jid, allow_registration) - local username, host = jid_split(user_jid); - local mod_invites, err = get_module(host, "invites"); - if not mod_invites then return nil, err or "mod_invites not loaded on this host"; end - local invite, err = mod_invites.create_contact(username, allow_registration); +module:add_item("shell-command", { + section = "invite"; + section_desc = "Create and manage invitations"; + name = "create_contact"; + desc = "Create an invitation to become contacts with the specified user"; + args = { { name = "user_jid", type = "string" }, { name = "allow_registration" } }; + host_selector = "user_jid"; + + handler = function (self, user_jid, allow_registration) --luacheck: ignore 212/self + local username = jid_split(user_jid); + local invite, err = create_contact(username, allow_registration); if not invite then return nil, err; end return true, invite.landing_page or invite.uri; - end -end + end; +}); + +local subcommands = {}; --- prosodyctl command function module.command(arg) - if #arg < 2 or arg[1] ~= "generate" then + local opts = argparse.parse(arg, { short_params = { h = "help"; ["?"] = "help" } }); + local cmd = table.remove(arg, 1); -- pop command + if opts.help or not cmd or not subcommands[cmd] then print("usage: prosodyctl mod_"..module.name.." generate example.com"); return 2; end - table.remove(arg, 1); -- pop command - - local sm = require "core.storagemanager"; - local mm = require "core.modulemanager"; + return subcommands[cmd](arg); +end - local host = arg[1]; - assert(prosody.hosts[host], "Host "..tostring(host).." does not exist"); +function subcommands.generate(arg) + local function help(short) + print("usage: prosodyctl mod_" .. module.name .. " generate DOMAIN --reset USERNAME") + print("usage: prosodyctl mod_" .. module.name .. " generate DOMAIN [--admin] [--role ROLE] [--group GROUPID]...") + if short then return 2 end + print() + print("This command has two modes: password reset and new account.") + print("If --reset is given, the command operates in password reset mode and in new account mode otherwise.") + print() + print("required arguments in password reset mode:") + print() + print(" --reset USERNAME Generate a password reset link for the given USERNAME.") + print() + print("optional arguments in new account mode:") + print() + print(" --admin Make the new user privileged") + print(" Equivalent to --role prosody:admin") + print(" --role ROLE Grant the given ROLE to the new user") + print(" --group GROUPID Add the user to the group with the given ID") + print(" Can be specified multiple times") + print(" --expires-after T Time until the invite expires (e.g. '1 week')") + print() + print("--group can be specified multiple times; the user will be added to all groups.") + print() + print("--reset and the other options cannot be mixed.") + return 2 + end + + local earlyopts = argparse.parse(arg, { short_params = { h = "help"; ["?"] = "help" } }); + if earlyopts.help or not earlyopts[1] then + return help(); + end + + local sm = require "prosody.core.storagemanager"; + local mm = require "prosody.core.modulemanager"; + + local host = table.remove(arg, 1); -- pop host + if not host then return help(true) end sm.initialize_host(host); - table.remove(arg, 1); -- pop host module.host = host; --luacheck: ignore 122/module token_storage = module:open_store("invite_token", "map"); + local opts = argparse.parse(arg, { + short_params = { h = "help"; ["?"] = "help"; g = "group" }; + value_params = { group = true; reset = true; role = true }; + array_params = { group = true; role = true }; + }); + + if opts.help then + return help(); + end + -- Load mod_invites local invites = module:depends("invites"); -- Optional community module that if used, needs to be loaded here @@ -257,71 +309,28 @@ end local allow_reset; - local roles; - local groups = {}; - while #arg > 0 do - local value = arg[1]; - table.remove(arg, 1); - if value == "--help" then - print("usage: prosodyctl mod_"..module.name.." generate DOMAIN --reset USERNAME") - print("usage: prosodyctl mod_"..module.name.." generate DOMAIN [--admin] [--role ROLE] [--group GROUPID]...") - print() - print("This command has two modes: password reset and new account.") - print("If --reset is given, the command operates in password reset mode and in new account mode otherwise.") - print() - print("required arguments in password reset mode:") - print() - print(" --reset USERNAME Generate a password reset link for the given USERNAME.") - print() - print("optional arguments in new account mode:") - print() - print(" --admin Make the new user privileged") - print(" Equivalent to --role prosody:admin") - print(" --role ROLE Grant the given ROLE to the new user") - print(" --group GROUPID Add the user to the group with the given ID") - print(" Can be specified multiple times") - print() - print("--role and --admin override each other; the last one wins") - print("--group can be specified multiple times; the user will be added to all groups.") - print() - print("--reset and the other options cannot be mixed.") - return 2 - elseif value == "--reset" then - local nodeprep = require "util.encodings".stringprep.nodeprep; - local username = nodeprep(arg[1]) - table.remove(arg, 1); - if not username then - print("Please supply a valid username to generate a reset link for"); - return 2; - end - allow_reset = username; - elseif value == "--admin" then - roles = { ["prosody:admin"] = true }; - elseif value == "--role" then - local rolename = arg[1]; - if not rolename then - print("Please supply a role name"); - return 2; - end - roles = { [rolename] = true }; - table.remove(arg, 1); - elseif value == "--group" or value == "-g" then - local groupid = arg[1]; - if not groupid then - print("Please supply a group ID") - return 2; - end - table.insert(groups, groupid); - table.remove(arg, 1); - else - print("unexpected argument: "..value) + if opts.reset then + local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; + local username = nodeprep(opts.reset) + if not username then + print("Please supply a valid username to generate a reset link for"); + return 2; end + allow_reset = username; + end + + local roles = opts.role or {}; + local groups = opts.groups or {}; + + if opts.admin then + -- Insert it first since we don't get order out of argparse + table.insert(roles, 1, "prosody:admin"); end local invite; if allow_reset then - if roles then + if roles[1] then print("--role/--admin and --reset are mutually exclusive") return 2; end @@ -333,7 +342,7 @@ invite = assert(invites.create_account(nil, { roles = roles, groups = groups - })); + }, opts.expires_after and human_io.parse_duration(opts.expires_after))); end print(invite.landing_page or invite.uri); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_invites_adhoc.lua --- a/plugins/mod_invites_adhoc.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_invites_adhoc.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,8 +1,7 @@ -- XEP-0401: Easy User Onboarding -local dataforms = require "util.dataforms"; -local datetime = require "util.datetime"; -local split_jid = require "util.jid".split; -local usermanager = require "core.usermanager"; +local dataforms = require "prosody.util.dataforms"; +local datetime = require "prosody.util.datetime"; +local split_jid = require "prosody.util.jid".split; local new_adhoc = module:require("adhoc").new; @@ -13,8 +12,7 @@ -- on the server, use the option above instead. local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true); -local allow_user_invite_roles = module:get_option_set("allow_user_invites_by_roles"); -local deny_user_invite_roles = module:get_option_set("deny_user_invites_by_roles"); +module:default_permission(allow_user_invites and "prosody:registered" or "prosody:admin", ":invite-users"); local invites; if prosody.shutdown then -- COMPAT hack to detect prosodyctl @@ -42,36 +40,8 @@ -- This is for checking if the specified JID may create invites -- that allow people to register accounts on this host. -local function may_invite_new_users(jid) - if usermanager.get_roles then - local user_roles = usermanager.get_roles(jid, module.host); - if not user_roles then - -- User has no roles we can check, just return default - return allow_user_invites; - end - - if user_roles["prosody:admin"] then - return true; - end - if allow_user_invite_roles then - for allowed_role in allow_user_invite_roles do - if user_roles[allowed_role] then - return true; - end - end - end - if deny_user_invite_roles then - for denied_role in deny_user_invite_roles do - if user_roles[denied_role] then - return false; - end - end - end - elseif usermanager.is_admin(jid, module.host) then -- COMPAT w/0.11 - return true; -- Admins may always create invitations - end - -- No role matches, so whatever the default is - return allow_user_invites; +local function may_invite_new_users(context) + return module:may(":invite-users", context); end module:depends("adhoc"); @@ -91,7 +61,7 @@ }; }; end - local invite = invites.create_contact(username, may_invite_new_users(data.from), { + local invite = invites.create_contact(username, may_invite_new_users(data), { source = data.from }); --TODO: check errors diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_invites_register.lua --- a/plugins/mod_invites_register.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_invites_register.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,7 @@ -local st = require "util.stanza"; -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local rostermanager = require "core.rostermanager"; +local st = require "prosody.util.stanza"; +local jid_split = require "prosody.util.jid".split; +local jid_bare = require "prosody.util.jid".bare; +local rostermanager = require "prosody.core.rostermanager"; local require_encryption = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); @@ -147,7 +147,20 @@ if validated_invite.additional_data then module:log("debug", "Importing roles from invite"); local roles = validated_invite.additional_data.roles; - if roles then + if roles and roles[1] ~= nil then + local um = require "prosody.core.usermanager"; + local ok, err = um.set_user_role(event.username, module.host, roles[1]); + if not ok then + module:log("error", "Could not set role %s for newly registered user %s: %s", roles[1], event.username, err); + end + for i = 2, #roles do + local ok, err = um.add_user_secondary_role(event.username, module.host, roles[i]); + if not ok then + module:log("warn", "Could not add secondary role %s for newly registered user %s: %s", roles[i], event.username, err); + end + end + elseif roles and type(next(roles)) == "string" then + module:log("warn", "Invite carries legacy, migration required for user '%s' for role set %q to take effect", event.username, roles); module:open_store("roles"):set(contact_username, roles); end end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_iq.lua --- a/plugins/mod_iq.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_iq.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,7 +7,7 @@ -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local full_sessions = prosody.full_sessions; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_lastactivity.lua --- a/plugins/mod_lastactivity.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_lastactivity.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,10 +6,10 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; -local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; +local st = require "prosody.util.stanza"; +local is_contact_subscribed = require "prosody.core.rostermanager".is_contact_subscribed; +local jid_bare = require "prosody.util.jid".bare; +local jid_split = require "prosody.util.jid".split; module:add_feature("jabber:iq:last"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_legacyauth.lua --- a/plugins/mod_legacyauth.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_legacyauth.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,17 +8,17 @@ -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local t_concat = table.concat; local secure_auth_only = module:get_option("c2s_require_encryption", module:get_option("require_encryption", true)) or not(module:get_option("allow_unencrypted_plain_auth")); -local sessionmanager = require "core.sessionmanager"; -local usermanager = require "core.usermanager"; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local resourceprep = require "util.encodings".stringprep.resourceprep; +local sessionmanager = require "prosody.core.sessionmanager"; +local usermanager = require "prosody.core.usermanager"; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local resourceprep = require "prosody.util.encodings".stringprep.resourceprep; module:add_feature("jabber:iq:auth"); module:hook("stream-features", function(event) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_limits.lua --- a/plugins/mod_limits.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_limits.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,13 +1,13 @@ -- Because we deal with pre-authed sessions and streams we can't be host-specific module:set_global(); -local filters = require "util.filters"; -local throttle = require "util.throttle"; -local timer = require "util.timer"; +local filters = require "prosody.util.filters"; +local throttle = require "prosody.util.throttle"; +local timer = require "prosody.util.timer"; local ceil = math.ceil; local limits_cfg = module:get_option("limits", {}); -local limits_resolution = module:get_option_number("limits_resolution", 1); +local limits_resolution = module:get_option_period("limits_resolution", 1); local default_bytes_per_second = 3000; local default_burst = 2; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_mam/mamprefs.lib.lua --- a/plugins/mod_mam/mamprefs.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_mam/mamprefs.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,11 +10,14 @@ -- -- luacheck: ignore 122/prosody -local global_default_policy = module:get_option_string("default_archive_policy", true); -if global_default_policy ~= "roster" then - global_default_policy = module:get_option_boolean("default_archive_policy", global_default_policy); +local global_default_policy = module:get_option_enum("default_archive_policy", "always", "roster", "never", true, false); +local smart_enable = module:get_option_boolean("mam_smart_enable", false); + +if global_default_policy == "always" then + global_default_policy = true; +elseif global_default_policy == "never" then + global_default_policy = false; end -local smart_enable = module:get_option_boolean("mam_smart_enable", false); do -- luacheck: ignore 211/prefs_format diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_mam/mamprefsxml.lib.lua --- a/plugins/mod_mam/mamprefsxml.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_mam/mamprefsxml.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,8 +10,8 @@ -- XEP-0313: Message Archive Management for Prosody -- -local st = require"util.stanza"; -local jid_prep = require"util.jid".prep; +local st = require"prosody.util.stanza"; +local jid_prep = require"prosody.util.jid".prep; local xmlns_mam = "urn:xmpp:mam:2"; local default_attrs = { diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_mam/mod_mam.lua --- a/plugins/mod_mam/mod_mam.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_mam/mod_mam.lua Wed Mar 27 15:39:03 2024 +0000 @@ -15,36 +15,36 @@ local xmlns_forward = "urn:xmpp:forward:0"; local xmlns_st_id = "urn:xmpp:sid:0"; -local um = require "core.usermanager"; -local st = require "util.stanza"; -local rsm = require "util.rsm"; +local um = require "prosody.core.usermanager"; +local st = require "prosody.util.stanza"; +local rsm = require "prosody.util.rsm"; local get_prefs = module:require"mamprefs".get; local set_prefs = module:require"mamprefs".set; local prefs_to_stanza = module:require"mamprefsxml".tostanza; local prefs_from_stanza = module:require"mamprefsxml".fromstanza; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; -local jid_resource = require "util.jid".resource; -local jid_prepped_split = require "util.jid".prepped_split; -local dataform = require "util.dataforms".new; -local get_form_type = require "util.dataforms".get_type; +local jid_bare = require "prosody.util.jid".bare; +local jid_split = require "prosody.util.jid".split; +local jid_resource = require "prosody.util.jid".resource; +local jid_prepped_split = require "prosody.util.jid".prepped_split; +local dataform = require "prosody.util.dataforms".new; +local get_form_type = require "prosody.util.dataforms".get_type; local host = module.host; -local rm_load_roster = require "core.rostermanager".load_roster; +local rm_load_roster = require "prosody.core.rostermanager".load_roster; local is_stanza = st.is_stanza; local tostring = tostring; -local time_now = os.time; +local time_now = require "prosody.util.time".now; local m_min = math.min; local timestamp, datestamp = import( "util.datetime", "datetime", "date"); -local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); +local default_max_items, max_max_items = 20, module:get_option_integer("max_archive_query_results", 50, 0); local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" }); local archive_store = module:get_option_string("archive_store", "archive"); local archive = module:open_store(archive_store, "archive"); -local cleanup_after = module:get_option_string("archive_expires_after", "1w"); -local archive_item_limit = module:get_option_number("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000); +local cleanup_after = module:get_option_period("archive_expires_after", "1w"); +local archive_item_limit = module:get_option_integer("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000, 0); local archive_truncate = math.floor(archive_item_limit * 0.99); if not archive.find then @@ -53,8 +53,12 @@ end local use_total = module:get_option_boolean("mam_include_total", true); -function schedule_cleanup() - -- replaced later if cleanup is enabled +function schedule_cleanup(_username, _date) -- luacheck: ignore 212 + -- Called to make a note of which users have messages on which days, which in + -- turn is used to optimize the message expiry routine. + -- + -- This noop is conditionally replaced later depending on retention settings + -- and storage backend capabilities. end -- Handle prefs. @@ -245,8 +249,7 @@ return true; end - local id, _, when = first(); - if id then + for id, _, when in first do reply:tag("start", { id = id, timestamp = timestamp(when) }):up(); end end @@ -258,8 +261,7 @@ return true; end - local id, _, when = last(); - if id then + for id, _, when in last do reply:tag("end", { id = id, timestamp = timestamp(when) }):up(); end end @@ -437,7 +439,7 @@ local time = time_now(); local ok, err = archive:append(store_user, nil, clone_for_storage, time, with); if not ok and err == "quota-limit" then - if type(cleanup_after) == "number" then + if cleanup_after ~= math.huge then module:log("debug", "User '%s' over quota, cleaning archive", store_user); local cleaned = archive:delete(store_user, { ["end"] = (os.time() - cleanup_after); @@ -502,20 +504,10 @@ end end); -if cleanup_after ~= "never" then +if cleanup_after ~= math.huge 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]?)"); - if not n then - module:log("error", "Could not parse archive_expires_after string %q", cleanup_after); - return false; - end - - cleanup_after = tonumber(n) * ( multipliers[m] or 1 ); - module:log("debug", "archive_expires_after = %d -- in seconds", cleanup_after); if not archive.delete then @@ -528,7 +520,7 @@ -- outside the cleanup range. if not (archive.caps and archive.caps.wildcard_delete) then - local last_date = require "util.cache".new(module:get_option_number("archive_cleanup_date_cache_size", 1000)); + local last_date = require "prosody.util.cache".new(module:get_option_integer("archive_cleanup_date_cache_size", 1000, 1)); function schedule_cleanup(username, date) date = date or datestamp(); if last_date:get(username) == date then return end @@ -541,7 +533,7 @@ local cleanup_time = module:measure("cleanup", "times"); - local async = require "util.async"; + local async = require "prosody.util.async"; module:daily("Remove expired messages", function () local cleanup_done = cleanup_time(); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_message.lua --- a/plugins/mod_message.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_message.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,10 +10,10 @@ local full_sessions = prosody.full_sessions; local bare_sessions = prosody.bare_sessions; -local st = require "util.stanza"; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; -local user_exists = require "core.usermanager".user_exists; +local st = require "prosody.util.stanza"; +local jid_bare = require "prosody.util.jid".bare; +local jid_split = require "prosody.util.jid".split; +local user_exists = require "prosody.core.usermanager".user_exists; local function process_to_bare(bare, origin, stanza) local user = bare_sessions[bare]; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_mimicking.lua --- a/plugins/mod_mimicking.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_mimicking.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,13 +6,13 @@ -- COPYING file in the source package for more information. -- -local encodings = require "util.encodings"; +local encodings = require "prosody.util.encodings"; assert(encodings.confusable, "This module requires that Prosody be built with ICU"); local skeleton = encodings.confusable.skeleton; -local usage = require "util.prosodyctl".show_usage; -local usermanager = require "core.usermanager"; -local storagemanager = require "core.storagemanager"; +local usage = require "prosody.util.prosodyctl".show_usage; +local usermanager = require "prosody.core.usermanager"; +local storagemanager = require "prosody.core.storagemanager"; local skeletons function module.load() diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_motd.lua --- a/plugins/mod_motd.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_motd.lua Wed Mar 27 15:39:03 2024 +0000 @@ -13,7 +13,7 @@ if not motd_text then return; end -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; motd_text = motd_text:gsub("^%s*(.-)%s*$", "%1"):gsub("\n[ \t]+", "\n"); -- Strip indentation from the config diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_muc_mam.lua --- a/plugins/mod_muc_mam.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_muc_mam.lua Wed Mar 27 15:39:03 2024 +0000 @@ -16,28 +16,28 @@ local xmlns_muc_user = "http://jabber.org/protocol/muc#user"; local muc_form_enable = "muc#roomconfig_enablearchiving" -local st = require "util.stanza"; -local rsm = require "util.rsm"; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; -local jid_prep = require "util.jid".prep; -local dataform = require "util.dataforms".new; -local get_form_type = require "util.dataforms".get_type; +local st = require "prosody.util.stanza"; +local rsm = require "prosody.util.rsm"; +local jid_bare = require "prosody.util.jid".bare; +local jid_split = require "prosody.util.jid".split; +local jid_prep = require "prosody.util.jid".prep; +local dataform = require "prosody.util.dataforms".new; +local get_form_type = require "prosody.util.dataforms".get_type; local mod_muc = module:depends"muc"; local get_room_from_jid = mod_muc.get_room_from_jid; local is_stanza = st.is_stanza; local tostring = tostring; -local time_now = os.time; +local time_now = require "prosody.util.time".now; local m_min = math.min; -local timestamp, datestamp = import("util.datetime", "datetime", "date"); -local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); +local timestamp, datestamp = import("prosody.util.datetime", "datetime", "date"); +local default_max_items, max_max_items = 20, module:get_option_integer("max_archive_query_results", 50, 0); -local cleanup_after = module:get_option_string("muc_log_expires_after", "1w"); +local cleanup_after = module:get_option_period("muc_log_expires_after", "1w"); local default_history_length = 20; -local max_history_length = module:get_option_number("max_history_messages", math.huge); +local max_history_length = module:get_option_integer("max_history_messages", math.huge, 0); local function get_historylength(room) return math.min(room._data.history_length or default_history_length, max_history_length); @@ -53,7 +53,7 @@ local archive_store = "muc_log"; local archive = module:open_store(archive_store, "archive"); -local archive_item_limit = module:get_option_number("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000); +local archive_item_limit = module:get_option_integer("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000, 0); local archive_truncate = math.floor(archive_item_limit * 0.99); if archive.name == "null" or not archive.find then @@ -397,7 +397,7 @@ local id, err = archive:append(room_node, nil, stored_stanza, time, with); if not id and err == "quota-limit" then - if type(cleanup_after) == "number" then + if cleanup_after ~= math.huge then module:log("debug", "Room '%s' over quota, cleaning archive", room_node); local cleaned = archive:delete(room_node, { ["end"] = (os.time() - cleanup_after); @@ -467,20 +467,10 @@ -- Cleanup -if cleanup_after ~= "never" then +if cleanup_after ~= math.huge then local cleanup_storage = module:open_store("muc_log_cleanup"); local cleanup_map = module:open_store("muc_log_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]?)"); - if not n then - module:log("error", "Could not parse muc_log_expires_after string %q", cleanup_after); - return false; - end - - cleanup_after = tonumber(n) * ( multipliers[m] or 1 ); - module:log("debug", "muc_log_expires_after = %d -- in seconds", cleanup_after); if not archive.delete then @@ -492,7 +482,7 @@ -- messages, we collect the union of sets of rooms from dates that fall -- outside the cleanup range. - local last_date = require "util.cache".new(module:get_option_number("muc_log_cleanup_date_cache_size", 1000)); + local last_date = require "prosody.util.cache".new(module:get_option_integer("muc_log_cleanup_date_cache_size", 1000, 1)); if not ( archive.caps and archive.caps.wildcard_delete ) then function schedule_cleanup(roomname, date) date = date or datestamp(); @@ -506,7 +496,7 @@ local cleanup_time = module:measure("cleanup", "times"); - local async = require "util.async"; + local async = require "prosody.util.async"; module:daily("Remove expired messages", function () local cleanup_done = cleanup_time(); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_muc_unique.lua --- a/plugins/mod_muc_unique.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_muc_unique.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,6 @@ -- XEP-0307: Unique Room Names for Multi-User Chat -local st = require "util.stanza"; -local unique_name = require "util.id".medium; +local st = require "prosody.util.stanza"; +local unique_name = require "prosody.util.id".medium; module:add_feature "http://jabber.org/protocol/muc#unique" module:hook("iq-get/host/http://jabber.org/protocol/muc#unique:unique", function(event) local origin, stanza = event.origin, event.stanza; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_net_multiplex.lua --- a/plugins/mod_net_multiplex.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_net_multiplex.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,10 @@ module:set_global(); -local array = require "util.array"; -local max_buffer_len = module:get_option_number("multiplex_buffer_size", 1024); -local default_mode = module:get_option_number("network_default_read_size", 4096); +local array = require "prosody.util.array"; +local max_buffer_len = module:get_option_integer("multiplex_buffer_size", 1024, 1); +local default_mode = module:get_option_integer("network_default_read_size", 4096, 0); -local portmanager = require "core.portmanager"; +local portmanager = require "prosody.core.portmanager"; local available_services = {}; local service_by_protocol = {}; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_offline.lua --- a/plugins/mod_offline.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_offline.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,8 +7,8 @@ -- -local datetime = require "util.datetime"; -local jid_split = require "util.jid".split; +local datetime = require "prosody.util.datetime"; +local jid_split = require "prosody.util.jid".split; local offline_messages = module:open_store("offline", "archive"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_pep.lua --- a/plugins/mod_pep.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_pep.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,16 +1,16 @@ -local pubsub = require "util.pubsub"; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; -local jid_join = require "util.jid".join; -local set_new = require "util.set".new; -local st = require "util.stanza"; -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 usermanager = require "core.usermanager"; +local pubsub = require "prosody.util.pubsub"; +local jid_bare = require "prosody.util.jid".bare; +local jid_split = require "prosody.util.jid".split; +local jid_join = require "prosody.util.jid".join; +local set_new = require "prosody.util.set".new; +local st = require "prosody.util.stanza"; +local calculate_hash = require "prosody.util.caps".calculate_hash; +local is_contact_subscribed = require "prosody.core.rostermanager".is_contact_subscribed; +local cache = require "prosody.util.cache"; +local set = require "prosody.util.set"; +local new_id = require "prosody.util.id".medium; +local storagemanager = require "prosody.core.storagemanager"; +local usermanager = require "prosody.core.usermanager"; local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; @@ -24,7 +24,7 @@ local pep_service_items = {}; -- size of caches with full pubsub service objects -local service_cache_size = module:get_option_number("pep_service_cache_size", 1000); +local service_cache_size = module:get_option_integer("pep_service_cache_size", 1000, 1); -- username -> util.pubsub service object local services = cache.new(service_cache_size, function (username, _) @@ -36,7 +36,7 @@ end):table(); -- size of caches with smaller objects -local info_cache_size = module:get_option_number("pep_info_cache_size", 10000); +local info_cache_size = module:get_option_integer("pep_info_cache_size", 10000, 1); -- username -> recipient -> set of nodes local recipients = cache.new(info_cache_size):table(); @@ -49,7 +49,7 @@ local node_config = module:open_store("pep", "map"); local known_nodes = module:open_store("pep"); -local max_max_items = module:get_option_number("pep_max_items", 256); +local max_max_items = module:get_option_number("pep_max_items", 256, 0); local function tonumber_max_items(n) if n == "max" then @@ -136,10 +136,14 @@ local function get_broadcaster(username) local user_bare = jid_join(username, host); local function simple_broadcast(kind, node, jids, item, _, node_obj) + local expose_publisher; if node_obj then if node_obj.config["notify_"..kind] == false then return; end + if node_obj.config.itemreply == "publisher" then + expose_publisher = true; + end end if kind == "retract" then kind = "items"; -- XEP-0060 signals retraction in an container @@ -151,6 +155,9 @@ if node_obj and node_obj.config.include_payload == false then item:maptags(function () return nil; end); end + if not expose_publisher then + item.attr.publisher = nil; + end end end @@ -306,7 +313,7 @@ if ok and config.send_last_published_item ~= "on_sub_and_presence" then return end local ok, id, item = service:get_last_item(node, jid); if not (ok and id) then return; end - service.config.broadcaster("items", node, { [jid] = true }, item); + service.config.broadcaster("items", node, { [jid] = true }, item, true, service.nodes[node], service); end local function update_subscriptions(recipient, service_name, nodes) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_pep_simple.lua --- a/plugins/mod_pep_simple.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_pep_simple.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,15 +7,15 @@ -- -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; -local st = require "util.stanza"; -local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local jid_bare = require "prosody.util.jid".bare; +local jid_split = require "prosody.util.jid".split; +local st = require "prosody.util.stanza"; +local is_contact_subscribed = require "prosody.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 unpack = table.unpack; +local calculate_hash = require "prosody.util.caps".calculate_hash; local core_post_stanza = prosody.core_post_stanza; local bare_sessions = prosody.bare_sessions; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_ping.lua --- a/plugins/mod_ping.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_ping.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,7 +6,7 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; module:add_feature("urn:xmpp:ping"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_posix.lua --- a/plugins/mod_posix.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_posix.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,167 +6,6 @@ -- COPYING file in the source package for more information. -- - -local want_pposix_version = "0.4.0"; - -local pposix = assert(require "util.pposix"); -if pposix._VERSION ~= want_pposix_version then - module:log("warn", "Unknown version (%s) of binary pposix module, expected %s." - .. "Perhaps you need to recompile?", tostring(pposix._VERSION), want_pposix_version); -end - -local have_signal, signal = pcall(require, "util.signal"); -if not have_signal then - module:log("warn", "Couldn't load signal library, won't respond to SIGTERM"); -end - -local lfs = require "lfs"; -local stat = lfs.attributes; - -local prosody = _G.prosody; - module:set_global(); -- we're a global module -local umask = module:get_option_string("umask", "027"); -pposix.umask(umask); - --- Don't even think about it! -if not prosody.start_time then -- server-starting - if pposix.getuid() == 0 and not module:get_option_boolean("run_as_root") then - module:log("error", "Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); - module:log("error", "For more information on running Prosody as root, see https://prosody.im/doc/root"); - prosody.shutdown("Refusing to run as root", 1); - end -end - -local pidfile; -local pidfile_handle; - -local function remove_pidfile() - if pidfile_handle then - pidfile_handle:close(); - os.remove(pidfile); - pidfile, pidfile_handle = nil, nil; - end -end - -local function write_pidfile() - if pidfile_handle then - remove_pidfile(); - end - pidfile = module:get_option_path("pidfile", nil, "data"); - if pidfile then - local err; - local mode = stat(pidfile) and "r+" or "w+"; - pidfile_handle, err = io.open(pidfile, mode); - if not pidfile_handle then - module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); - prosody.shutdown("Couldn't write pidfile", 1); - else - if not lfs.lock(pidfile_handle, "w") then -- Exclusive lock - local other_pid = pidfile_handle:read("*a"); - module:log("error", "Another Prosody instance seems to be running with PID %s, quitting", other_pid); - pidfile_handle = nil; - prosody.shutdown("Prosody already running", 1); - else - pidfile_handle:close(); - pidfile_handle, err = io.open(pidfile, "w+"); - if not pidfile_handle then - module:log("error", "Couldn't write pidfile at %s; %s", pidfile, err); - prosody.shutdown("Couldn't write pidfile", 1); - else - if lfs.lock(pidfile_handle, "w") then - pidfile_handle:write(tostring(pposix.getpid())); - pidfile_handle:flush(); - end - end - end - end - end -end - -local daemonize = prosody.opts.daemonize; - -if daemonize == nil then - -- Fall back to config file if not specified on command-line - daemonize = module:get_option_boolean("daemonize", nil); - if daemonize ~= nil then - module:log("warn", "The 'daemonize' option has been deprecated, specify -D or -F on the command line instead."); - -- TODO: Write some docs and include a link in the warning. - end -end - -local function remove_log_sinks() - local lm = require "core.loggingmanager"; - lm.register_sink_type("console", nil); - lm.register_sink_type("stdout", nil); - lm.reload_logging(); -end - -if daemonize then - local function daemonize_server() - module:log("info", "Prosody is about to detach from the console, disabling further console output"); - remove_log_sinks(); - local ok, ret = pposix.daemonize(); - if not ok then - module:log("error", "Failed to daemonize: %s", ret); - elseif ret and ret > 0 then - os.exit(0); - else - module:log("info", "Successfully daemonized to PID %d", pposix.getpid()); - write_pidfile(); - end - end - module:hook("server-started", daemonize_server) -else - -- Not going to daemonize, so write the pid of this process - write_pidfile(); -end - -module:hook("server-stopped", remove_pidfile); - --- Set signal handlers -if have_signal then - module:add_timer(0, function () - signal.signal("SIGTERM", function () - module:log("warn", "Received SIGTERM"); - prosody.main_thread:run(function () - prosody.unlock_globals(); - prosody.shutdown("Received SIGTERM"); - prosody.lock_globals(); - end); - end); - - signal.signal("SIGHUP", function () - module:log("info", "Received SIGHUP"); - prosody.main_thread:run(function () - prosody.reload_config(); - end); - -- this also reloads logging - end); - - signal.signal("SIGINT", function () - module:log("info", "Received SIGINT"); - prosody.main_thread:run(function () - prosody.unlock_globals(); - prosody.shutdown("Received SIGINT"); - prosody.lock_globals(); - end); - end); - - signal.signal("SIGUSR1", function () - module:log("info", "Received SIGUSR1"); - module:fire_event("signal/SIGUSR1"); - end); - - signal.signal("SIGUSR2", function () - module:log("info", "Received SIGUSR2"); - module:fire_event("signal/SIGUSR2"); - end); - end); -end - --- For other modules to reference -features = { - signal_events = true; -}; +-- TODO delete this whole concept diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_presence.lua --- a/plugins/mod_presence.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_presence.lua Wed Mar 27 15:39:03 2024 +0000 @@ -15,19 +15,19 @@ local core_post_stanza = prosody.core_post_stanza; local core_process_stanza = prosody.core_process_stanza; -local st = require "util.stanza"; -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local datetime = require "util.datetime"; +local st = require "prosody.util.stanza"; +local jid_split = require "prosody.util.jid".split; +local jid_bare = require "prosody.util.jid".bare; +local datetime = require "prosody.util.datetime"; local hosts = prosody.hosts; local bare_sessions = prosody.bare_sessions; local full_sessions = prosody.full_sessions; local NULL = {}; -local rostermanager = require "core.rostermanager"; -local sessionmanager = require "core.sessionmanager"; +local rostermanager = require "prosody.core.rostermanager"; +local sessionmanager = require "prosody.core.sessionmanager"; -local recalc_resource_map = require "util.presence".recalc_resource_map; +local recalc_resource_map = require "prosody.util.presence".recalc_resource_map; local ignore_presence_priority = module:get_option_boolean("ignore_presence_priority", false); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_private.lua --- a/plugins/mod_private.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_private.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,7 +7,7 @@ -- -local st = require "util.stanza" +local st = require "prosody.util.stanza" local private_storage = module:open_store("private", "map"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_proxy65.lua --- a/plugins/mod_proxy65.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_proxy65.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,11 +9,11 @@ module:set_global(); -local jid_compare, jid_prep = require "util.jid".compare, require "util.jid".prep; -local st = require "util.stanza"; -local sha1 = require "util.hashes".sha1; -local server = require "net.server"; -local portmanager = require "core.portmanager"; +local jid_compare, jid_prep = require "prosody.util.jid".compare, require "prosody.util.jid".prep; +local st = require "prosody.util.stanza"; +local sha1 = require "prosody.util.hashes".sha1; +local server = require "prosody.net.server"; +local portmanager = require "prosody.core.portmanager"; local sessions = module:shared("sessions"); local transfers = module:shared("transfers"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_pubsub/mod_pubsub.lua --- a/plugins/mod_pubsub/mod_pubsub.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_pubsub/mod_pubsub.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,9 @@ -local pubsub = require "util.pubsub"; -local st = require "util.stanza"; -local jid_bare = require "util.jid".bare; -local usermanager = require "core.usermanager"; -local new_id = require "util.id".medium; -local storagemanager = require "core.storagemanager"; -local xtemplate = require "util.xtemplate"; +local pubsub = require "prosody.util.pubsub"; +local st = require "prosody.util.stanza"; +local jid_bare = require "prosody.util.jid".bare; +local new_id = require "prosody.util.id".medium; +local storagemanager = require "prosody.core.storagemanager"; +local xtemplate = require "prosody.util.xtemplate"; local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; @@ -13,7 +12,7 @@ local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); local pubsub_disco_name = module:get_option_string("name", "Prosody PubSub Service"); -local expose_publisher = module:get_option_boolean("expose_publisher", false) +local service_expose_publisher = module:get_option_boolean("expose_publisher") local service; @@ -40,7 +39,7 @@ -- get(node_name) -- users(): iterator over (node_name) -local max_max_items = module:get_option_number("pubsub_max_items", 256); +local max_max_items = module:get_option_integer("pubsub_max_items", 256, 1); local function tonumber_max_items(n) if n == "max" then @@ -82,7 +81,11 @@ if node_obj and node_obj.config.include_payload == false then item:maptags(function () return nil; end); end - if not expose_publisher then + local node_expose_publisher = service_expose_publisher; + if node_expose_publisher == nil and node_obj and node_obj.config.itemreply == "publisher" then + node_expose_publisher = true; + end + if not node_expose_publisher then item.attr.publisher = nil; elseif not item.attr.publisher and actor ~= true then item.attr.publisher = service.config.normalize_jid(actor); @@ -136,12 +139,22 @@ -- Compose a textual representation of Atom payloads local summary_templates = module:get_option("pubsub_summary_templates", { - ["http://www.w3.org/2005/Atom"] = "{summary|or{{author/name|and{{author/name} posted }}{title}}}"; + ["http://www.w3.org/2005/Atom"] = "{@pubsub:title|and{*{@pubsub:title}*\n\n}}{summary|or{{author/name|and{{author/name} posted }}{title}}}"; }) for pubsub_type, template in pairs(summary_templates) do module:hook("pubsub-summary/"..pubsub_type, function (event) local payload = event.payload; + + local got_config, node_config = service:get_node_config(event.node, true); + if got_config then + payload = st.clone(payload); + payload.attr["xmlns:pubsub"] = xmlns_pubsub; + payload.attr["pubsub:node"] = event.node; + payload.attr["pubsub:title"] = node_config.title; + payload.attr["pubsub:description"] = node_config.description; + end + return xtemplate.render(template, payload, tostring); end, -1); end @@ -176,10 +189,11 @@ end end); -local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); +local admin_aff = module:get_option_enum("default_admin_affiliation", "owner", "publisher", "member", "outcast", "none"); +module:default_permission("prosody:admin", ":service-admin"); local function get_affiliation(jid) local bare_jid = jid_bare(jid); - if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then + if bare_jid == module.host or module:may(":service-admin", bare_jid) then return admin_aff; end end @@ -192,7 +206,7 @@ service = new_service; service.config.autocreate_on_publish = autocreate_on_publish; service.config.autocreate_on_subscribe = autocreate_on_subscribe; - service.config.expose_publisher = expose_publisher; + service.config.expose_publisher = service_expose_publisher; service.config.nodestore = node_store; service.config.itemstore = create_simple_itemstore; @@ -219,7 +233,7 @@ set_service(pubsub.new({ autocreate_on_publish = autocreate_on_publish; autocreate_on_subscribe = autocreate_on_subscribe; - expose_publisher = expose_publisher; + expose_publisher = service_expose_publisher; node_defaults = { ["persist_items"] = true; @@ -236,3 +250,46 @@ normalize_jid = jid_bare; })); end + +local function get_service(service_jid) + return assert(assert(prosody.hosts[service_jid], "Unknown pubsub service").modules.pubsub, "Not a pubsub service").service; +end + +module:add_item("shell-command", { + section = "pubsub"; + section_desc = "Manage publish/subscribe nodes"; + name = "create_node"; + desc = "Create a node with the specified name"; + args = { + { name = "service_jid", type = "string" }; + { name = "node_name", type = "string" }; + }; + host_selector = "service_jid"; + + handler = function (self, service_jid, node_name) --luacheck: ignore 212/self + return get_service(service_jid):create(node_name, true); + end; +}); + +module:add_item("shell-command", { + section = "pubsub"; + section_desc = "Manage publish/subscribe nodes"; + name = "list_nodes"; + desc = "List nodes on a pubsub service"; + args = { + { name = "service_jid", type = "string" }; + }; + host_selector = "service_jid"; + + handler = function (self, service_jid) --luacheck: ignore 212/self + -- luacheck: ignore 431/service + local service = get_service(service_jid); + local nodes = select(2, assert(service:get_nodes(true))); + local count = 0; + for node_name in pairs(nodes) do + count = count + 1; + self.session.print(node_name); + end + return true, ("%d nodes"):format(count); + end; +}); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_pubsub/pubsub.lib.lua --- a/plugins/mod_pubsub/pubsub.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_pubsub/pubsub.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,13 +1,13 @@ -local t_unpack = table.unpack or unpack; -- luacheck: ignore 113 +local t_unpack = table.unpack; local time_now = os.time; -local jid_prep = require "util.jid".prep; -local set = require "util.set"; -local st = require "util.stanza"; -local it = require "util.iterators"; -local uuid_generate = require "util.uuid".generate; -local dataform = require"util.dataforms".new; -local errors = require "util.error"; +local jid_prep = require "prosody.util.jid".prep; +local set = require "prosody.util.set"; +local st = require "prosody.util.stanza"; +local it = require "prosody.util.iterators"; +local uuid_generate = require "prosody.util.uuid".generate; +local dataform = require"prosody.util.dataforms".new; +local errors = require "prosody.util.error"; local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; @@ -164,6 +164,17 @@ var = "pubsub#notify_retract"; value = true; }; + { + type = "list-single"; + label = "Specify whose JID to include as the publisher of items"; + name = "itemreply"; + var = "pubsub#itemreply"; + options = { + { label = "Include the node owner's JID", value = "owner" }; + { label = "Include the item publisher's JID", value = "publisher" }; + { label = "Don't include any JID with items", value = "none", default = true }; + }; + }; }; _M.node_config_form = node_config_form; @@ -347,6 +358,13 @@ origin.send(pubsub_error_reply(stanza, "nodeid-required")); return true; end + + local node_obj = service.nodes[node]; + if not node_obj then + origin.send(pubsub_error_reply(stanza, "item-not-found")); + return true; + end + local resultspec; -- TODO rsm.get() if items.attr.max_items then resultspec = { max = tonumber(items.attr.max_items) }; @@ -358,6 +376,9 @@ end local expose_publisher = service.config.expose_publisher; + if expose_publisher == nil and node_obj.config.itemreply == "publisher" then + expose_publisher = true; + end local data = st.stanza("items", { node = node }); local iter, v, i = ipairs(results); @@ -678,8 +699,7 @@ function handlers.set_retract(origin, stanza, retract, service) local node, notify = retract.attr.node, retract.attr.notify; notify = (notify == "1") or (notify == "true"); - local item = retract:get_child("item"); - local id = item and item.attr.id + local id = retract:get_child_attr("item", nil, "id"); if not (node and id) then origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); return true; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_register_ibr.lua --- a/plugins/mod_register_ibr.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_register_ibr.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,19 +7,21 @@ -- -local st = require "util.stanza"; -local dataform_new = require "util.dataforms".new; -local usermanager_user_exists = require "core.usermanager".user_exists; -local usermanager_create_user = require "core.usermanager".create_user; -local usermanager_set_password = require "core.usermanager".create_user; -local usermanager_delete_user = require "core.usermanager".delete_user; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local util_error = require "util.error"; +local st = require "prosody.util.stanza"; +local dataform_new = require "prosody.util.dataforms".new; +local usermanager_user_exists = require "prosody.core.usermanager".user_exists; +local usermanager_create_user_with_role = require "prosody.core.usermanager".create_user_with_role; +local usermanager_set_password = require "prosody.core.usermanager".create_user; +local usermanager_delete_user = require "prosody.core.usermanager".delete_user; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local util_error = require "prosody.util.error"; -local additional_fields = module:get_option("additional_registration_fields", {}); +local additional_fields = module:get_option_array("additional_registration_fields", {}); local require_encryption = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); +local default_role = module:get_option_string("register_ibr_default_role", "prosody:registered"); + pcall(function () module:depends("register_limits"); end); @@ -166,7 +168,12 @@ return true; end - local user = { username = username, password = password, host = host, additional = data, ip = session.ip, session = session, allowed = true } + local user = { + username = username, password = password, host = host; + additional = data, ip = session.ip, session = session; + role = default_role; + allowed = true; + }; module:fire_event("user-registering", user); if not user.allowed then local error_type, error_condition, reason; @@ -200,7 +207,7 @@ end end - local created, err = usermanager_create_user(username, password, host); + local created, err = usermanager_create_user_with_role(username, password, host, user.role); if created then data.registered = os.time(); if not account_details:set(username, data) then diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_register_limits.lua --- a/plugins/mod_register_limits.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_register_limits.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,23 +7,23 @@ -- -local create_throttle = require "util.throttle".create; -local new_cache = require "util.cache".new; -local ip_util = require "util.ip"; +local create_throttle = require "prosody.util.throttle".create; +local new_cache = require "prosody.util.cache".new; +local ip_util = require "prosody.util.ip"; local new_ip = ip_util.new_ip; local match_ip = ip_util.match; local parse_cidr = ip_util.parse_cidr; -local errors = require "util.error"; +local errors = require "prosody.util.error"; -- COMPAT drop old option names -local min_seconds_between_registrations = module:get_option_number("min_seconds_between_registrations"); +local min_seconds_between_registrations = module:get_option_period("min_seconds_between_registrations"); local allowlist_only = module:get_option_boolean("allowlist_registration_only", module:get_option_boolean("whitelist_registration_only")); local allowlisted_ips = module:get_option_set("registration_allowlist", module:get_option("registration_whitelist", { "127.0.0.1", "::1" }))._items; local blocklisted_ips = module:get_option_set("registration_blocklist", module:get_option_set("registration_blacklist", {}))._items; -local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1); -local throttle_period = module:get_option_number("registration_throttle_period", min_seconds_between_registrations); -local throttle_cache_size = module:get_option_number("registration_throttle_cache_size", 100); +local throttle_max = module:get_option_number("registration_throttle_max", min_seconds_between_registrations and 1, 0); +local throttle_period = module:get_option_period("registration_throttle_period", min_seconds_between_registrations); +local throttle_cache_size = module:get_option_integer("registration_throttle_cache_size", 100, 1); local blocklist_overflow = module:get_option_boolean("blocklist_on_registration_throttle_overload", module:get_option_boolean("blacklist_on_registration_throttle_overload", false)); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_roster.lua --- a/plugins/mod_roster.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_roster.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,18 +7,18 @@ -- -local st = require "util.stanza" +local st = require "prosody.util.stanza" -local jid_split = require "util.jid".split; -local jid_resource = require "util.jid".resource; -local jid_prep = require "util.jid".prep; +local jid_split = require "prosody.util.jid".split; +local jid_resource = require "prosody.util.jid".resource; +local jid_prep = require "prosody.util.jid".prep; local tonumber = tonumber; local pairs = pairs; -local rm_load_roster = require "core.rostermanager".load_roster; -local rm_remove_from_roster = require "core.rostermanager".remove_from_roster; -local rm_add_to_roster = require "core.rostermanager".add_to_roster; -local rm_roster_push = require "core.rostermanager".roster_push; +local rm_load_roster = require "prosody.core.rostermanager".load_roster; +local rm_remove_from_roster = require "prosody.core.rostermanager".remove_from_roster; +local rm_add_to_roster = require "prosody.core.rostermanager".add_to_roster; +local rm_roster_push = require "prosody.core.rostermanager".roster_push; module:add_feature("jabber:iq:roster"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_s2s.lua --- a/plugins/mod_s2s.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_s2s.lua Wed Mar 27 15:39:03 2024 +0000 @@ -16,32 +16,32 @@ local t_insert = table.insert; local traceback = debug.traceback; -local add_task = require "util.timer".add_task; -local stop_timer = require "util.timer".stop; -local st = require "util.stanza"; -local initialize_filters = require "util.filters".initialize; -local nameprep = require "util.encodings".stringprep.nameprep; -local new_xmpp_stream = require "util.xmppstream".new; -local s2s_new_incoming = require "core.s2smanager".new_incoming; -local s2s_new_outgoing = require "core.s2smanager".new_outgoing; -local s2s_destroy_session = require "core.s2smanager".destroy_session; -local uuid_gen = require "util.uuid".generate; -local async = require "util.async"; +local add_task = require "prosody.util.timer".add_task; +local stop_timer = require "prosody.util.timer".stop; +local st = require "prosody.util.stanza"; +local initialize_filters = require "prosody.util.filters".initialize; +local nameprep = require "prosody.util.encodings".stringprep.nameprep; +local new_xmpp_stream = require "prosody.util.xmppstream".new; +local s2s_new_incoming = require "prosody.core.s2smanager".new_incoming; +local s2s_new_outgoing = require "prosody.core.s2smanager".new_outgoing; +local s2s_destroy_session = require "prosody.core.s2smanager".destroy_session; +local uuid_gen = require "prosody.util.uuid".generate; +local async = require "prosody.util.async"; local runner = async.runner; -local connect = require "net.connect".connect; -local service = require "net.resolvers.service"; -local resolver_chain = require "net.resolvers.chain"; -local errors = require "util.error"; -local set = require "util.set"; +local connect = require "prosody.net.connect".connect; +local service = require "prosody.net.resolvers.service"; +local resolver_chain = require "prosody.net.resolvers.chain"; +local errors = require "prosody.util.error"; +local set = require "prosody.util.set"; -local connect_timeout = module:get_option_number("s2s_timeout", 90); -local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5); +local connect_timeout = module:get_option_period("s2s_timeout", 90); +local stream_close_timeout = module:get_option_period("s2s_close_timeout", 5); local opt_keepalives = module:get_option_boolean("s2s_tcp_keepalives", module:get_option_boolean("tcp_keepalives", true)); local secure_auth = module:get_option_boolean("s2s_secure_auth", false); -- One day... 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_number("s2s_stanza_size_limit", 1024*512); +local stanza_size_limit = module:get_option_integer("s2s_stanza_size_limit", 1024*512, 10000); local measure_connections_inbound = module:metric( "gauge", "connections_inbound", "", @@ -95,6 +95,12 @@ }; local s2s_service_options_mt = { __index = s2s_service_options } +if module:get_option_boolean("use_dane", false) then + -- DANE is supported in net.connect but only for outgoing connections, + -- to authenticate incoming connections with DANE we need + module:depends("s2s_auth_dane_in"); +end + module:hook("stats-update", function () measure_connections_inbound:clear() measure_connections_outbound:clear() @@ -146,17 +152,17 @@ elseif type(reason) == "string" then reason_text = reason; end - for i, data in ipairs(sendq) do - local reply = data[2]; - if reply and not(reply.attr.xmlns) and bouncy_stanzas[reply.name] then - reply.attr.type = "error"; - reply:tag("error", {type = error_type, by = session.from_host}) - :tag(condition, {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}):up(); - if reason_text then - reply:tag("text", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"}) - :text("Server-to-server connection failed: "..reason_text):up(); - end + for i, stanza in ipairs(sendq) do + if not stanza.attr.xmlns and bouncy_stanzas[stanza.name] and stanza.attr.type ~= "error" and stanza.attr.type ~= "result" then + local reply = st.error_reply( + stanza, + error_type, + condition, + reason_text and ("Server-to-server connection failed: "..reason_text) or nil + ); core_process_stanza(dummy, reply); + else + (session.log or log)("debug", "Not eligible for bouncing, discarding %s", stanza:top_tag()); end sendq[i] = nil; end @@ -182,15 +188,11 @@ (host.log or log)("debug", "trying to send over unauthed s2sout to "..to_host); -- Queue stanza until we are able to send it - local queued_item = { - tostring(stanza), - stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza); - }; if host.sendq then - t_insert(host.sendq, queued_item); + t_insert(host.sendq, st.clone(stanza)); else -- luacheck: ignore 122 - host.sendq = { queued_item }; + host.sendq = { st.clone(stanza) }; end host.log("debug", "stanza [%s] queued ", stanza.name); return true; @@ -215,7 +217,7 @@ -- Store in buffer host_session.bounce_sendq = bounce_sendq; - host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} }; + host_session.sendq = { st.clone(stanza) }; log("debug", "stanza [%s] queued until connection complete", stanza.name); -- FIXME Cleaner solution to passing extra data from resolvers to net.server -- This mt-clone allows resolvers to add extra data, currently used for DANE TLSA records @@ -255,9 +257,32 @@ end 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 stanza_size_limit then + event.features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }) + :text_tag("max-bytes", string.format("%d", stanza_size_limit)):up(); + end + end); + module:hook_tag("urn:xmpp:bidi", "bidi", function(session, stanza) + -- Advertising features on bidi connections where no is sent in the other direction + local limits = stanza:get_child("limits", "urn:xmpp:stream-limits:0"); + if limits then + session.outgoing_stanza_size_limit = tonumber(limits:get_child_text("max-bytes")); + end + end, 100); module:hook("s2s-authenticated", make_authenticated, -1); module:hook("s2s-read-timeout", keepalive, -1); + module:hook("smacks-ack-delayed", function (event) + if event.origin.type == "s2sin" or event.origin.type == "s2sout" then + event.origin:close("connection-timeout"); + return true; + end + end, -1); module:hook_stanza("http://etherx.jabber.org/streams", "features", function (session, stanza) -- luacheck: ignore 212/stanza + local limits = stanza:get_child("limits", "urn:xmpp:stream-limits:0"); + if limits then + session.outgoing_stanza_size_limit = tonumber(limits:get_child_text("max-bytes")); + end if session.type == "s2sout" then -- Stream is authenticated and we are seem to be done with feature negotiation, -- so the stream is ready for stanzas. RFC 6120 Section 4.3 @@ -283,7 +308,7 @@ function module.unload() if module.reloading then return end for _, session in pairs(sessions) do - if session.to_host == module.host or session.from_host == module.host then + if session.host == module.host then session:close("host-gone"); end end @@ -328,8 +353,8 @@ if sendq then session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host); local send = session.sends2s; - for i, data in ipairs(sendq) do - send(data[1]); + for i, stanza in ipairs(sendq) do + send(stanza); sendq[i] = nil; end session.sendq = nil; @@ -393,10 +418,10 @@ --- Helper to check that a session peer's certificate is valid local function check_cert_status(session) local host = session.direction == "outgoing" and session.to_host or session.from_host - local conn = session.conn:socket() + local conn = session.conn local cert - if conn.getpeercertificate then - cert = conn:getpeercertificate() + if conn.ssl_peercertificate then + cert = conn:ssl_peercertificate() end return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); @@ -408,8 +433,7 @@ session.secure = true; session.encrypted = true; - local sock = session.conn:socket(); - local info = sock.info and sock:info(); + local info = session.conn:ssl_info(); if type(info) == "table" then (session.log or log)("info", "Stream encrypted (%s with %s)", info.protocol, info.cipher); session.compressed = info.compression; @@ -438,7 +462,8 @@ session.had_stream = true; -- Had a stream opened at least once -- TODO: Rename session.secure to session.encrypted - if session.secure == false then + if session.secure == false then -- Set by mod_tls during STARTTLS handshake + session.starttls = "completed"; session_secure(session); end @@ -526,6 +551,12 @@ end if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then + if stanza_size_limit then + features:reset(); + features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }) + :text_tag("max-bytes", string.format("%d", stanza_size_limit)):up(); + end + log("debug", "Sending stream features: %s", features); session.sends2s(features); else @@ -760,6 +791,7 @@ local w = conn.write; if conn:ssl() then + -- Direct TLS was used session_secure(session); end @@ -770,6 +802,11 @@ end if t then t = filter("bytes/out", tostring(t)); + if session.outgoing_stanza_size_limit and #t > session.outgoing_stanza_size_limit then + log("warn", "Attempt to send a stanza exceeding session limit of %dB (%dB)!", session.outgoing_stanza_size_limit, #t); + -- TODO Pass identifiable error condition back to allow appropriate handling + return false + end if t then return w(conn, t); end @@ -938,6 +975,18 @@ return "has expired"; elseif cert_errors:contains("self signed certificate") then return "is self-signed"; + elseif cert_errors:contains("no matching DANE TLSA records") then + return "does not match any DANE TLSA records"; + end + + local chain_errors = set.new(session.cert_chain_errors[2]); + for i, e in pairs(session.cert_chain_errors) do + if i > 2 then chain_errors:add_list(e); end + end + if chain_errors:contains("certificate has expired") then + return "has an expired certificate chain"; + elseif chain_errors:contains("no matching DANE TLSA records") then + return "does not match any DANE TLSA records"; end end return "is not trusted"; -- for some other reason @@ -966,6 +1015,8 @@ -- In practice most cases are configuration mistakes or forgotten -- certificate renewals. We think it's better to let the other party -- know about the problem so that they can fix it. + -- + -- Note: Bounce message must not include name of server, as it may leak half your JID in semi-anon MUCs. session:close({ condition = "not-authorized", text = "Your server's certificate "..reason }, nil, "Remote server's certificate "..reason); return false; @@ -976,7 +1027,7 @@ module:hook("server-stopping", function(event) -- Close ports - local pm = require "core.portmanager"; + local pm = require "prosody.core.portmanager"; for _, netservice in pairs(module.items["net-provider"]) do pm.unregister_service(netservice.name, netservice); end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_s2s_auth_certs.lua --- a/plugins/mod_s2s_auth_certs.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_s2s_auth_certs.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,6 @@ module:set_global(); -local cert_verify_identity = require "util.x509".verify_identity; -local NULL = {}; +local cert_verify_identity = require "prosody.util.x509".verify_identity; local log = module._log; local measure_cert_statuses = module:metric("counter", "checked", "", "Certificate validation results", @@ -9,25 +8,26 @@ module:hook("s2s-check-certificate", function(event) local session, host, cert = event.session, event.host, event.cert; - local conn = session.conn:socket(); + local conn = session.conn; local log = session.log or log; + local secure_hostname = conn.extra and conn.extra.secure_hostname; + if not cert then log("warn", "No certificate provided by %s", host or "unknown host"); return; end - local chain_valid, errors; - if conn.getpeerverification then - chain_valid, errors = conn:getpeerverification(); - else - chain_valid, errors = false, { { "Chain verification not supported by this version of LuaSec" } }; - end + local chain_valid, errors = conn:ssl_peerverification(); -- Is there any interest in printing out all/the number of errors here? if not chain_valid then log("debug", "certificate chain validation result: invalid"); - for depth, t in pairs(errors or NULL) do - log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", ")) + if type(errors) == "table" then + for depth, t in pairs(errors) do + log("debug", "certificate error(s) at depth %d: %s", depth-1, table.concat(t, ", ")); + end + else + log("debug", "certificate error: %s", errors); end session.cert_chain_status = "invalid"; session.cert_chain_errors = errors; @@ -45,6 +45,14 @@ end log("debug", "certificate identity validation result: %s", session.cert_identity_status); end + + -- Check for DNSSEC-signed SRV hostname + if secure_hostname and session.cert_identity_status ~= "valid" then + if cert_verify_identity(secure_hostname, "xmpp-server", cert) then + module:log("info", "Secure SRV name delegation %q -> %q", secure_hostname, host); + session.cert_identity_status = "valid" + end + end end measure_cert_statuses:with_labels(session.cert_chain_status or "unknown", session.cert_identity_status or "unknown"):add(1); end, 509); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_s2s_auth_dane_in.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/mod_s2s_auth_dane_in.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,130 @@ +module:set_global(); + +local dns = require "prosody.net.adns"; +local async = require "prosody.util.async"; +local encodings = require "prosody.util.encodings"; +local hashes = require "prosody.util.hashes"; +local promise = require "prosody.util.promise"; +local x509 = require "prosody.util.x509"; + +local idna_to_ascii = encodings.idna.to_ascii; +local sha256 = hashes.sha256; +local sha512 = hashes.sha512; + +local use_dane = module:get_option_boolean("use_dane", nil); +if use_dane == nil then + module:log("warn", "DANE support incomplete, add use_dane = true in the global section to support outgoing s2s connections"); +elseif use_dane == false then + module:log("debug", "DANE support disabled with use_dane = false, disabling.") + return +end + +local function ensure_secure(r) + assert(r.secure, "insecure"); + return r; +end + +local function ensure_nonempty(r) + assert(r[1], "empty"); + return r; +end + +local function flatten(a) + local seen = {}; + local ret = {}; + for _, rrset in ipairs(a) do + for _, rr in ipairs(rrset) do + if not seen[tostring(rr)] then + table.insert(ret, rr); + seen[tostring(rr)] = true; + end + end + end + return ret; +end + +local lazy_tlsa_mt = { + __index = function(t, i) + if i == 1 then + local h = sha256(t[0]); + t[1] = h; + return h; + elseif i == 2 then + local h = sha512(t[0]); + t[1] = h; + return h; + end + end; +} +local function lazy_hash(t) + return setmetatable(t, lazy_tlsa_mt); +end + +module:hook("s2s-check-certificate", function(event) + local session, host, cert = event.session, event.host, event.cert; + local log = session.log or module._log; + + if not host or not cert or session.direction ~= "incoming" then + return + end + + local by_select_match = { + [0] = lazy_hash { + -- cert + [0] = x509.pem2der(cert:pem()); + + }; + } + if cert.pubkey then + by_select_match[1] = lazy_hash { + -- spki + [0] = x509.pem2der(cert:pubkey()); + }; + end + + local resolver = dns.resolver(); + + local dns_domain = idna_to_ascii(host); + + local function fetch_tlsa(res) + local tlsas = {}; + for _, rr in ipairs(res) do + if rr.srv.target == "." then return {}; end + table.insert(tlsas, resolver:lookup_promise(("_%d._tcp.%s"):format(rr.srv.port, rr.srv.target), "TLSA"):next(ensure_secure)); + end + return promise.all(tlsas):next(flatten); + end + + local ret = async.wait_for(resolver:lookup_promise("_xmpp-server." .. dns_domain, "TLSA"):next(ensure_secure):next(ensure_nonempty):catch(function() + return promise.all({ + resolver:lookup_promise("_xmpps-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa); + resolver:lookup_promise("_xmpp-server._tcp." .. dns_domain, "SRV"):next(ensure_secure):next(fetch_tlsa); + }):next(flatten); + end)); + + if not ret then + return + end + + local found_supported = false; + for _, rr in ipairs(ret) do + if rr.tlsa.use == 3 and by_select_match[rr.tlsa.select] and rr.tlsa.match <= 2 then + found_supported = true; + if rr.tlsa.data == by_select_match[rr.tlsa.select][rr.tlsa.match] then + module:log("debug", "%s matches", rr) + session.cert_chain_status = "valid"; + session.cert_identity_status = "valid"; + return true; + end + else + log("debug", "Unsupported DANE TLSA record: %s", rr); + end + end + + if found_supported then + session.cert_chain_status = "invalid"; + session.cert_identity_status = nil; + return true; + end + +end, 800); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_s2s_bidi.lua --- a/plugins/mod_s2s_bidi.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_s2s_bidi.lua Wed Mar 27 15:39:03 2024 +0000 @@ -5,7 +5,7 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local xmlns_bidi_feature = "urn:xmpp:features:bidi" local xmlns_bidi = "urn:xmpp:bidi"; @@ -25,7 +25,9 @@ if bidi then session.incoming = true; session.log("debug", "Requesting bidirectional stream"); - session.sends2s(st.stanza("bidi", { xmlns = xmlns_bidi })); + local request_bidi = st.stanza("bidi", { xmlns = xmlns_bidi }); + module:fire_event("s2sout-stream-features", { origin = session, features = request_bidi }); + session.sends2s(request_bidi); end end end, 200); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_saslauth.lua --- a/plugins/mod_saslauth.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_saslauth.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,19 +8,26 @@ -- luacheck: ignore 431/log -local st = require "util.stanza"; -local sm_bind_resource = require "core.sessionmanager".bind_resource; -local sm_make_authenticated = require "core.sessionmanager".make_authenticated; -local base64 = require "util.encodings".base64; -local set = require "util.set"; -local errors = require "util.error"; +local st = require "prosody.util.stanza"; +local sm_bind_resource = require "prosody.core.sessionmanager".bind_resource; +local sm_make_authenticated = require "prosody.core.sessionmanager".make_authenticated; +local base64 = require "prosody.util.encodings".base64; +local set = require "prosody.util.set"; +local errors = require "prosody.util.error"; +local hex = require "prosody.util.hex"; +local pem2der = require"util.x509".pem2der; +local hashes = require"util.hashes"; +local ssl = require "ssl"; -- FIXME Isolate LuaSec from the rest of the code -local usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; +local certmanager = require "core.certmanager"; +local pm_get_tls_config_at = require "prosody.core.portmanager".get_tls_config_at; +local usermanager_get_sasl_handler = require "prosody.core.usermanager".get_sasl_handler; local secure_auth_only = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); local allow_unencrypted_plain_auth = module:get_option_boolean("allow_unencrypted_plain_auth", false) local insecure_mechanisms = module:get_option_set("insecure_sasl_mechanisms", allow_unencrypted_plain_auth and {} or {"PLAIN", "LOGIN"}); local disabled_mechanisms = module:get_option_set("disable_sasl_mechanisms", { "DIGEST-MD5" }); +local tls_server_end_point_hash = module:get_option_string("tls_server_end_point_hash"); local log = module._log; @@ -49,11 +56,14 @@ return "failure", "temporary-auth-failure", "Connection gone"; end if status == "failure" then - module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg }); + local event = { session = session, condition = ret, text = err_msg }; + module:fire_event("authentication-failure", event); session.sasl_handler = session.sasl_handler:clean_clone(); + ret, err_msg = event.condition, event.text; elseif status == "success" then - local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.scope); + local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.role); if ok then + session.sasl_resource = session.sasl_handler.resource; module:fire_event("authentication-success", { session = session }); session.sasl_handler = nil; session:reset_stream(); @@ -77,9 +87,12 @@ return true; end end - local status, ret, err_msg = session.sasl_handler:process(text); + local sasl_handler = session.sasl_handler; + local status, ret, err_msg = sasl_handler:process(text); status, ret, err_msg = handle_status(session, status, ret, err_msg); - local s = build_reply(status, ret, err_msg); + local event = { session = session, message = ret, error_text = err_msg }; + module:fire_event("sasl/"..session.base_type.."/"..status, event); + local s = build_reply(status, event.message, event.error_text); session.send(s); return true; end @@ -205,6 +218,12 @@ if session.type ~= "c2s_unauthed" or module:get_host_type() ~= "local" then return; end + -- event for preemptive checks, rate limiting etc + module:fire_event("authentication-attempt", event); + if event.allowed == false then + session.send(build_reply("failure", event.error_condition or "not-authorized", event.error_text)); + return true; + end if session.sasl_handler and session.sasl_handler.selected then session.sasl_handler = nil; -- allow starting a new SASL negotiation before completing an old one end @@ -242,7 +261,53 @@ end); local function tls_unique(self) - return self.userdata["tls-unique"]:getpeerfinished(); + return self.userdata["tls-unique"]:ssl_peerfinished(); +end + +local function tls_exporter(conn) + if not conn.ssl_exportkeyingmaterial then return end + return conn:ssl_exportkeyingmaterial("EXPORTER-Channel-Binding", 32, ""); +end + +local function sasl_tls_exporter(self) + return tls_exporter(self.userdata["tls-exporter"]); +end + +local function tls_server_end_point(self) + local cert_hash = self.userdata["tls-server-end-point"]; + if cert_hash then return hex.from(cert_hash); end + + local conn = self.userdata["tls-server-end-point-conn"]; + local cert = conn.getlocalcertificate and conn:getlocalcertificate(); + + if not cert then + -- We don't know that this is the right cert, it could have been replaced on + -- disk since we started. + local certfile = self.userdata["tls-server-end-point-cert"]; + if not certfile then return end + local f = io.open(certfile); + if not f then return end + local certdata = f:read("*a"); + f:close(); + cert = ssl.loadcertificate(certdata); + end + + -- Hash function selection, see RFC 5929 §4.1 + local hash, hash_name = hashes.sha256, "sha256"; + if cert.getsignaturename then + local sigalg = cert:getsignaturename():lower():match("sha%d+"); + if sigalg and sigalg ~= "sha1" and hashes[sigalg] then + -- This should have ruled out MD5 and SHA1 + hash, hash_name = hashes[sigalg], sigalg; + end + end + + local certdata_der = pem2der(cert:pem()); + local hashed_der = hash(certdata_der); + + module:log("debug", "tls-server-end-point: hex(%s(der)) = %q, hash = %s", hash_name, hex.encode(hashed_der)); + + return hashed_der; end local mechanisms_attr = { xmlns='urn:ietf:params:xml:ns:xmpp-sasl' }; @@ -258,22 +323,58 @@ end local sasl_handler = usermanager_get_sasl_handler(module.host, origin) origin.sasl_handler = sasl_handler; + local channel_bindings = set.new() if origin.encrypted then -- check whether LuaSec has the nifty binding to the function needed for tls-unique -- FIXME: would be nice to have this check only once and not for every socket if sasl_handler.add_cb_handler then - local socket = origin.conn:socket(); - local info = socket.info and socket:info(); - if info.protocol == "TLSv1.3" then + local info = origin.conn:ssl_info(); + if info and info.protocol == "TLSv1.3" then log("debug", "Channel binding 'tls-unique' undefined in context of TLS 1.3"); - elseif socket.getpeerfinished and socket:getpeerfinished() then + if tls_exporter(origin.conn) then + log("debug", "Channel binding 'tls-exporter' supported"); + sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); + channel_bindings:add("tls-exporter"); + end + elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported"); sasl_handler:add_cb_handler("tls-unique", tls_unique); + channel_bindings:add("tls-unique"); else log("debug", "Channel binding 'tls-unique' not supported (by LuaSec?)"); end + + local certfile; + if tls_server_end_point_hash == "auto" then + tls_server_end_point_hash = nil; + local ssl_cfg = origin.ssl_cfg; + if not ssl_cfg then + local server = origin.conn:server(); + local tls_config = pm_get_tls_config_at(server:ip(), server:serverport()); + local autocert = certmanager.find_host_cert(origin.conn:socket():getsniname()); + ssl_cfg = autocert or tls_config; + end + + certfile = ssl_cfg and ssl_cfg.certificate; + if certfile then + log("debug", "Channel binding 'tls-server-end-point' can be offered based on the certificate used"); + sasl_handler:add_cb_handler("tls-server-end-point", tls_server_end_point); + channel_bindings:add("tls-server-end-point"); + else + log("debug", "Channel binding 'tls-server-end-point' set to 'auto' but cannot determine cert"); + end + elseif tls_server_end_point_hash then + log("debug", "Channel binding 'tls-server-end-point' can be offered with the configured certificate hash"); + sasl_handler:add_cb_handler("tls-server-end-point", tls_server_end_point); + channel_bindings:add("tls-server-end-point"); + end + sasl_handler["userdata"] = { - ["tls-unique"] = socket; + ["tls-unique"] = origin.conn; + ["tls-exporter"] = origin.conn; + ["tls-server-end-point-cert"] = certfile; + ["tls-server-end-point-conn"] = origin.conn; + ["tls-server-end-point"] = tls_server_end_point_hash; }; else log("debug", "Channel binding not supported by SASL handler"); @@ -306,6 +407,14 @@ mechanisms:tag("mechanism"):text(mechanism):up(); end features:add_child(mechanisms); + if not channel_bindings:empty() then + -- XXX XEP-0440 is Experimental + features:tag("sasl-channel-binding", {xmlns='urn:xmpp:sasl-cb:0'}) + for channel_binding in channel_bindings do + features:tag("channel-binding", {type=channel_binding}):up() + end + features:up(); + end return; end @@ -328,7 +437,7 @@ authmod, available_disabled); end - else + elseif not origin.full_jid then features:tag("bind", bind_attr):tag("required"):up():up(); features:tag("session", xmpp_session_attr):tag("optional"):up():up(); end @@ -350,14 +459,15 @@ module:hook("stanza/iq/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event) local origin, stanza = event.origin, event.stanza; - local resource; - if stanza.attr.type == "set" then + local resource = origin.sasl_resource; + if stanza.attr.type == "set" and not resource then local bind = stanza.tags[1]; resource = bind:get_child("resource"); resource = resource and #resource.tags == 0 and resource[1] or nil; end local success, err_type, err, err_msg = sm_bind_resource(origin, resource); if success then + origin.sasl_resource = nil; origin.send(st.reply(stanza) :tag("bind", { xmlns = xmlns_bind }) :tag("jid"):text(origin.full_jid)); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_scansion_record.lua --- a/plugins/mod_scansion_record.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_scansion_record.lua Wed Mar 27 15:39:03 2024 +0000 @@ -2,11 +2,11 @@ local devices = { "", "phone", "laptop", "tablet", "toaster", "fridge", "shoe" }; local users = {}; -local filters = require "util.filters"; -local id = require "util.id"; -local dt = require "util.datetime"; -local dm = require "util.datamanager"; -local st = require "util.stanza"; +local filters = require "prosody.util.filters"; +local id = require "prosody.util.id"; +local dt = require "prosody.util.datetime"; +local dm = require "prosody.util.datamanager"; +local st = require "prosody.util.stanza"; local record_id = id.short():lower(); local record_date = os.date("%Y%b%d"):lower(); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_server_contact_info.lua --- a/plugins/mod_server_contact_info.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_server_contact_info.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,21 +6,22 @@ -- COPYING file in the source package for more information. -- -local array = require "util.array"; -local jid = require "util.jid"; +local array = require "prosody.util.array"; +local jid = require "prosody.util.jid"; local url = require "socket.url"; +module:depends("server_info"); + -- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo -local form_layout = require "util.dataforms".new({ - { var = "FORM_TYPE"; type = "hidden"; value = "http://jabber.org/network/serverinfo"; }; - { name = "abuse", var = "abuse-addresses", type = "list-multi" }, - { name = "admin", var = "admin-addresses", type = "list-multi" }, - { name = "feedback", var = "feedback-addresses", type = "list-multi" }, - { name = "sales", var = "sales-addresses", type = "list-multi" }, - { name = "security", var = "security-addresses", type = "list-multi" }, - { name = "status", var = "status-addresses", type = "list-multi" }, - { name = "support", var = "support-addresses", type = "list-multi" }, -}); +local address_types = { + abuse = "abuse-addresses"; + admin = "admin-addresses"; + feedback = "feedback-addresses"; + sales = "sales-addresses"; + security = "security-addresses"; + status = "status-addresses"; + support = "support-addresses"; +}; -- JIDs of configured service admins are used as fallback local admins = module:get_option_inherited_set("admins", {}); @@ -29,4 +30,17 @@ admin = array.collect(admins / jid.prep / function(admin) return url.build({scheme = "xmpp"; path = admin}); end); }); -module:add_extension(form_layout:form(contact_config, "result")); +local fields = {}; + +for key, field_var in pairs(address_types) do + if contact_config[key] then + table.insert(fields, { + type = "list-multi"; + name = key; + var = field_var; + value = contact_config[key]; + }); + end +end + +module:add_item("server-info-fields", fields); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_server_info.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/mod_server_info.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,55 @@ +local dataforms = require "prosody.util.dataforms"; + +local server_info_config = module:get_option("server_info", {}); +local server_info_custom_fields = module:get_option_array("server_info_extensions"); + +-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo +local form_layout = dataforms.new({ + { var = "FORM_TYPE"; type = "hidden"; value = "http://jabber.org/network/serverinfo" }; +}); + +if server_info_custom_fields then + for _, field in ipairs(server_info_custom_fields) do + table.insert(form_layout, field); + end +end + +local generated_form; + +function update_form() + local new_form = form_layout:form(server_info_config, "result"); + if generated_form then + module:remove_item("extension", generated_form); + end + generated_form = new_form; + module:add_item("extension", generated_form); +end + +function add_fields(event) + local fields = event.item; + for _, field in ipairs(fields) do + table.insert(form_layout, field); + end + update_form(); +end + +function remove_fields(event) + local removed_fields = event.item; + for _, removed_field in ipairs(removed_fields) do + local removed_var = removed_field.var or removed_field.name; + for i, field in ipairs(form_layout) do + local var = field.var or field.name + if var == removed_var then + table.remove(form_layout, i); + break; + end + end + end + update_form(); +end + +module:handle_items("server-info-fields", add_fields, remove_fields); + +function module.load() + update_form(); +end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_smacks.lua --- a/plugins/mod_smacks.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_smacks.lua Wed Mar 27 15:39:03 2024 +0000 @@ -2,7 +2,7 @@ -- -- Copyright (C) 2010-2015 Matthew Wild -- Copyright (C) 2010 Waqas Hussain --- Copyright (C) 2012-2021 Kim Alvefur +-- Copyright (C) 2012-2022 Kim Alvefur -- Copyright (C) 2012 Thijs Alkemade -- Copyright (C) 2014 Florian Zeitz -- Copyright (C) 2016-2020 Thilo Molitor @@ -10,6 +10,7 @@ -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- +-- TODO unify sendq and smqueue local tonumber = tonumber; local tostring = tostring; @@ -38,23 +39,23 @@ "histogram", "resumption_age", "seconds", "time the session had been hibernating at the time of a resumption", {}, - {buckets = { 0, 1, 2, 5, 10, 30, 60, 120, 300, 600 }} + {buckets = {0, 1, 12, 60, 360, 900, 1440, 3600, 14400, 86400}} ):with_labels(); local sessions_expired = module:measure("sessions_expired", "counter"); local sessions_started = module:measure("sessions_started", "counter"); -local datetime = require "util.datetime"; -local add_filter = require "util.filters".add_filter; -local jid = require "util.jid"; -local smqueue = require "util.smqueue"; -local st = require "util.stanza"; -local timer = require "util.timer"; -local new_id = require "util.id".short; -local watchdog = require "util.watchdog"; -local it = require"util.iterators"; +local datetime = require "prosody.util.datetime"; +local add_filter = require "prosody.util.filters".add_filter; +local jid = require "prosody.util.jid"; +local smqueue = require "prosody.util.smqueue"; +local st = require "prosody.util.stanza"; +local timer = require "prosody.util.timer"; +local new_id = require "prosody.util.id".short; +local watchdog = require "prosody.util.watchdog"; +local it = require"prosody.util.iterators"; -local sessionmanager = require "core.sessionmanager"; +local sessionmanager = require "prosody.core.sessionmanager"; local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas"; local xmlns_delay = "urn:xmpp:delay"; @@ -65,14 +66,14 @@ local sm2_attr = { xmlns = xmlns_sm2 }; local sm3_attr = { xmlns = xmlns_sm3 }; -local queue_size = module:get_option_number("smacks_max_queue_size", 500); -local resume_timeout = module:get_option_number("smacks_hibernation_time", 600); +local queue_size = module:get_option_integer("smacks_max_queue_size", 500, 1); +local resume_timeout = module:get_option_period("smacks_hibernation_time", "10 minutes"); local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", true); local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false); -local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0); -local max_inactive_unacked_stanzas = module:get_option_number("smacks_max_inactive_unacked_stanzas", 256); -local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30); -local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10); +local max_unacked_stanzas = module:get_option_integer("smacks_max_unacked_stanzas", 0, 0); +local max_inactive_unacked_stanzas = module:get_option_integer("smacks_max_inactive_unacked_stanzas", 256, 0); +local delayed_ack_timeout = module:get_option_period("smacks_max_ack_delay", 30); +local max_old_sessions = module:get_option_integer("smacks_max_old_sessions", 10, 0); local c2s_sessions = module:shared("/*/c2s/sessions"); local local_sessions = prosody.hosts[module.host].sessions; @@ -83,13 +84,43 @@ local old_session_registry = module:open_store("smacks_h", "map"); local session_registry = module:shared "/*/smacks/resumption-tokens"; -- > user@host/resumption-token --> resource -local ack_errors = require"util.error".init("mod_smacks", xmlns_sm3, { +local function registry_key(session, id) + return jid.join(session.username, session.host, id or session.resumption_token); +end + +local function track_session(session, id) + session_registry[registry_key(session, id)] = session; + session.resumption_token = id; +end + +local function save_old_session(session) + session_registry[registry_key(session)] = nil; + return old_session_registry:set(session.username, session.resumption_token, + { h = session.handled_stanza_count; t = os.time() }) +end + +local function clear_old_session(session, id) + session_registry[registry_key(session, id)] = nil; + return old_session_registry:set(session.username, id or session.resumption_token, nil) +end + +local ack_errors = require"prosody.util.error".init("mod_smacks", xmlns_sm3, { head = { condition = "undefined-condition"; text = "Client acknowledged more stanzas than sent by server" }; tail = { condition = "undefined-condition"; text = "Client acknowledged less stanzas than already acknowledged" }; pop = { condition = "internal-server-error"; text = "Something went wrong with Stream Management" }; overflow = { condition = "resource-constraint", text = "Too many unacked stanzas remaining, session can't be resumed" } }); +local enable_errors = require "prosody.util.error".init("mod_smacks", xmlns_sm3, { + already_enabled = { condition = "unexpected-request", text = "Stream management is already enabled" }; + bind_required = { condition = "unexpected-request", text = "Client must bind a resource before enabling stream management" }; + unavailable = { condition = "service-unavailable", text = "Stream management is not available for this stream" }; + -- Resumption + expired = { condition = "item-not-found", text = "Session expired, and cannot be resumed" }; + already_bound = { condition = "unexpected-request", text = "Cannot resume another session after a resource is bound" }; + unknown_session = { condition = "item-not-found", text = "Unknown session" }; +}); + -- COMPAT note the use of compatibility wrapper in events (queue:table()) local function ack_delayed(session, stanza) @@ -104,18 +135,18 @@ end local function can_do_smacks(session, advertise_only) - if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end + if session.smacks then return false, enable_errors.new("already_enabled"); end local session_type = session.type; if session.username then if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm - return false, "unexpected-request", "Client must bind a resource before enabling stream management"; + return false, enable_errors.new("bind_required"); end return true; elseif s2s_smacks and (session_type == "s2sin" or session_type == "s2sout") then return true; end - return false, "service-unavailable", "Stream management is not available for this stream"; + return false, enable_errors.new("unavailable"); end module:hook("stream-features", @@ -155,13 +186,12 @@ local function request_ack(session, reason) local queue = session.outgoing_stanza_queue; - session.log("debug", "Sending (inside timer, before send) from %s - #queue=%d", reason, queue:count_unacked()); + session.log("debug", "Sending from %s - #queue=%d", reason, queue:count_unacked()); session.awaiting_ack = true; (session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks })) if session.destroyed then return end -- sending something can trigger destruction -- expected_h could be lower than this expression e.g. more stanzas added to the queue meanwhile) session.last_requested_h = queue:count_acked() + queue:count_unacked(); - session.log("debug", "Sending (inside timer, after send) from %s - #queue=%d", reason, queue:count_unacked()); if not session.delayed_ack_timer then session.delayed_ack_timer = timer.add_task(delayed_ack_timeout, function() ack_delayed(session, nil); -- we don't know if this is the only new stanza in the queue @@ -180,7 +210,6 @@ -- supposed to be nil. -- However, when using mod_smacks with mod_websocket, then mod_websocket's -- stanzas/out filter can get called before this one and adds the xmlns. - if session.resending_unacked then return stanza end if not session.smacks then return stanza end local is_stanza = st.is_stanza(stanza) and (not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client') @@ -234,8 +263,7 @@ if session.smacks == nil then return end if session.resumption_token then session.log("debug", "Revoking resumption token"); - session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil; - old_session_registry:set(session.username, session.resumption_token, nil); + clear_old_session(session); session.resumption_token = nil; else session.log("debug", "Session not resumable"); @@ -274,17 +302,16 @@ return session; end -function handle_enable(session, stanza, xmlns_sm) - local ok, err, err_text = can_do_smacks(session); +function do_enable(session, stanza) + local ok, err = can_do_smacks(session); if not ok then - session.log("warn", "Failed to enable smacks: %s", err_text); -- TODO: XEP doesn't say we can send error text, should it? - (session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):tag(err, { xmlns = xmlns_errors})); - return true; + session.log("warn", "Failed to enable smacks: %s", err.text); -- TODO: XEP doesn't say we can send error text, should it? + return nil, err; end if session.username then local old_sessions, err = all_old_sessions:get(session.username); - module:log("debug", "Old sessions: %q", old_sessions) + session.log("debug", "Old sessions: %q", old_sessions) if old_sessions then local keep, count = {}, 0; for token, info in it.sorted_pairs(old_sessions, function(a, b) @@ -296,54 +323,73 @@ end all_old_sessions:set(session.username, keep); elseif err then - module:log("error", "Unable to retrieve old resumption counters: %s", err); + session.log("error", "Unable to retrieve old resumption counters: %s", err); end end - module:log("debug", "Enabling stream management"); - session.smacks = xmlns_sm; - - wrap_session(session, false); - - local resume_max; local resume_token; local resume = stanza.attr.resume; if (resume == "true" or resume == "1") and session.username then -- resumption on s2s is not currently supported resume_token = new_id(); - session_registry[jid.join(session.username, session.host, resume_token)] = session; - session.resumption_token = resume_token; - resume_max = tostring(resume_timeout); end - (session.sends2s or session.send)(st.stanza("enabled", { xmlns = xmlns_sm, id = resume_token, resume = resume, max = resume_max })); + + return { + type = "enabled"; + id = resume_token; + resume_max = resume_token and tostring(resume_timeout) or nil; + session = session; + finish = function () + session.log("debug", "Enabling stream management"); + + session.smacks = stanza.attr.xmlns; + if resume_token then + track_session(session, resume_token); + end + wrap_session(session, false); + end; + }; +end + +function handle_enable(session, stanza, xmlns_sm) + local enabled, err = do_enable(session, stanza); + if not enabled then + (session.sends2s or session.send)(st.stanza("failed", { xmlns = xmlns_sm }):add_error(err)); + return true; + end + + (session.sends2s or session.send)(st.stanza("enabled", { + xmlns = xmlns_sm; + id = enabled.id; + resume = enabled.id and "true" or nil; -- COMPAT w/ Conversations 2.10.10 requires 'true' not '1' + max = enabled.resume_max; + })); + + session.smacks = xmlns_sm; + enabled.finish(); + return true; end module:hook_tag(xmlns_sm2, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm2); end, 100); module:hook_tag(xmlns_sm3, "enable", function (session, stanza) return handle_enable(session, stanza, xmlns_sm3); end, 100); -module:hook_tag("http://etherx.jabber.org/streams", "features", - function (session, stanza) - -- Needs to be done after flushing sendq since those aren't stored as - -- stanzas and counting them is weird. - -- TODO unify sendq and smqueue - timer.add_task(1e-6, function () - if can_do_smacks(session) then - if stanza:get_child("sm", xmlns_sm3) then - session.sends2s(st.stanza("enable", sm3_attr)); - session.smacks = xmlns_sm3; - elseif stanza:get_child("sm", xmlns_sm2) then - session.sends2s(st.stanza("enable", sm2_attr)); - session.smacks = xmlns_sm2; - else - return; - end - wrap_session_out(session, false); - end - end); - end); +module:hook_tag("http://etherx.jabber.org/streams", "features", function(session, stanza) + if can_do_smacks(session) then + session.smacks_feature = stanza:get_child("sm", xmlns_sm3) or stanza:get_child("sm", xmlns_sm2); + end +end); + +module:hook("s2sout-established", function (event) + local session = event.session; + if not session.smacks_feature then return end + + session.smacks = session.smacks_feature.attr.xmlns; + wrap_session_out(session, false); + session.sends2s(st.stanza("enable", { xmlns = session.smacks })); +end); function handle_enabled(session, stanza, xmlns_sm) -- luacheck: ignore 212/stanza - module:log("debug", "Enabling stream management"); + session.log("debug", "Enabling stream management"); session.smacks = xmlns_sm; wrap_session_in(session, false); @@ -357,10 +403,10 @@ function handle_r(origin, stanza, xmlns_sm) -- luacheck: ignore 212/stanza if not origin.smacks then - module:log("debug", "Received ack request from non-smack-enabled session"); + origin.log("debug", "Received ack request from non-smack-enabled session"); return; end - module:log("debug", "Received ack request, acking for %d", origin.handled_stanza_count); + origin.log("debug", "Received ack request, acking for %d", origin.handled_stanza_count); -- Reply with (origin.sends2s or origin.send)(st.stanza("a", { xmlns = xmlns_sm, h = format_h(origin.handled_stanza_count) })); -- piggyback our own ack request if needed (see request_ack_if_needed() for explanation of last_requested_h) @@ -413,13 +459,14 @@ local queue = session.outgoing_stanza_queue; local unacked = queue:count_unacked() if unacked > 0 then + local error_from = jid.join(session.username, session.host or module.host); tx_dropped_stanzas:sample(unacked); session.smacks = false; -- Disable queueing session.outgoing_stanza_queue = nil; for stanza in queue._queue:consume() do if not module:fire_event("delivery/failure", { session = session, stanza = stanza }) then if stanza.attr.type ~= "error" and stanza.attr.from ~= session.full_jid then - local reply = st.error_reply(stanza, "cancel", "recipient-unavailable"); + local reply = st.error_reply(stanza, "cancel", "recipient-unavailable", nil, error_from); module:send(reply); end end @@ -495,11 +542,8 @@ end session.log("debug", "Destroying session for hibernating too long"); - session_registry[jid.join(session.username, session.host, session.resumption_token)] = nil; - old_session_registry:set(session.username, session.resumption_token, - { h = session.handled_stanza_count; t = os.time() }); + save_old_session(session); session.resumption_token = nil; - session.resending_unacked = true; -- stop outgoing_stanza_filter from re-queueing anything anymore sessionmanager.destroy_session(session, "Hibernating too long"); sessions_expired(1); end); @@ -533,131 +577,110 @@ module:hook("s2sout-destroyed", handle_s2s_destroyed); module:hook("s2sin-destroyed", handle_s2s_destroyed); -local function get_session_id(session) - return session.id or (tostring(session):match("[a-f0-9]+$")); -end - -function handle_resume(session, stanza, xmlns_sm) +function do_resume(session, stanza) if session.full_jid then session.log("warn", "Tried to resume after resource binding"); - session.send(st.stanza("failed", { xmlns = xmlns_sm }) - :tag("unexpected-request", { xmlns = xmlns_errors }) - ); - return true; + return nil, enable_errors.new("already_bound"); end local id = stanza.attr.previd; - local original_session = session_registry[jid.join(session.username, session.host, id)]; + local original_session = session_registry[registry_key(session, id)]; if not original_session then local old_session = old_session_registry:get(session.username, id); if old_session then session.log("debug", "Tried to resume old expired session with id %s", id); - session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(old_session.h) }) - :tag("item-not-found", { xmlns = xmlns_errors }) - ); - old_session_registry:set(session.username, id, nil); + clear_old_session(session, id); resumption_expired(1); - else - session.log("debug", "Tried to resume non-existent session with id %s", id); - session.send(st.stanza("failed", { xmlns = xmlns_sm }) - :tag("item-not-found", { xmlns = xmlns_errors }) - ); - end; - else - if original_session.hibernating_watchdog then - original_session.log("debug", "Letting the watchdog go"); - original_session.hibernating_watchdog:cancel(); - original_session.hibernating_watchdog = nil; - elseif session.hibernating then - original_session.log("error", "Hibernating session has no watchdog!") + return nil, enable_errors.new("expired", { h = old_session.h }); end - -- zero age = was not hibernating yet - local age = 0; - if original_session.hibernating then - local now = os_time(); - age = now - original_session.hibernating; - end - session.log("debug", "mod_smacks resuming existing session %s...", get_session_id(original_session)); - original_session.log("debug", "mod_smacks session resumed from %s...", get_session_id(session)); - -- TODO: All this should move to sessionmanager (e.g. session:replace(new_session)) - if original_session.conn then - original_session.log("debug", "mod_smacks closing an old connection for this session"); - local conn = original_session.conn; - c2s_sessions[conn] = nil; - conn:close(); - end + session.log("debug", "Tried to resume non-existent session with id %s", id); + return nil, enable_errors.new("unknown_session"); + end + + if original_session.hibernating_watchdog then + original_session.log("debug", "Letting the watchdog go"); + original_session.hibernating_watchdog:cancel(); + original_session.hibernating_watchdog = nil; + elseif session.hibernating then + original_session.log("error", "Hibernating session has no watchdog!") + end + -- zero age = was not hibernating yet + local age = 0; + if original_session.hibernating then + local now = os_time(); + age = now - original_session.hibernating; + end + + session.log("debug", "mod_smacks resuming existing session %s...", original_session.id); + + local queue = original_session.outgoing_stanza_queue; + local h = tonumber(stanza.attr.h); + + original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked()) + local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked + + if not err and not queue:resumable() then + err = ack_errors.new("overflow"); + end + + if err then + session.log("debug", "Resumption failed: %s", err); + return nil, err; + end + + -- Update original_session with the parameters (connection, etc.) from the new session + sessionmanager.update_session(original_session, session); - local migrated_session_log = session.log; - original_session.ip = session.ip; - original_session.conn = session.conn; - original_session.rawsend = session.rawsend; - original_session.rawsend.session = original_session; - original_session.rawsend.conn = original_session.conn; - original_session.send = session.send; - original_session.send.session = original_session; - original_session.close = session.close; - original_session.filter = session.filter; - original_session.filter.session = original_session; - original_session.filters = session.filters; - original_session.send.filter = original_session.filter; - original_session.stream = session.stream; - original_session.secure = session.secure; - original_session.hibernating = nil; - original_session.resumption_counter = (original_session.resumption_counter or 0) + 1; - session.log = original_session.log; - session.type = original_session.type; - wrap_session(original_session, true); - -- Inform xmppstream of the new session (passed to its callbacks) - original_session.stream:set_session(original_session); - -- Similar for connlisteners - c2s_sessions[session.conn] = original_session; + return { + type = "resumed"; + session = original_session; + id = id; + -- Return function to complete the resumption and resync unacked stanzas + -- This is two steps so we can support SASL2/ISR + finish = function () + -- Ok, we need to re-send any stanzas that the client didn't see + -- ...they are what is now left in the outgoing stanza queue + -- We have to use the send of "session" because we don't want to add our resent stanzas + -- to the outgoing queue again - local queue = original_session.outgoing_stanza_queue; - local h = tonumber(stanza.attr.h); + original_session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked()); + for _, queued_stanza in queue:resume() do + original_session.send(queued_stanza); + end + original_session.log("debug", "all stanzas resent, enabling stream management on resumed stream, #queue = %d", queue:count_unacked()); - original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked()) - local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked - - if not err and not queue:resumable() then - err = ack_errors.new("overflow"); - end + -- Add our own handlers to the resumed session (filters have been reset in the update) + wrap_session(original_session, true); - if err or not queue:resumable() then - original_session.send(st.stanza("failed", - { xmlns = xmlns_sm; h = format_h(original_session.handled_stanza_count); previd = id })); - original_session:close(err); - return false; - end - - original_session.send(st.stanza("resumed", { xmlns = xmlns_sm, - h = format_h(original_session.handled_stanza_count), previd = id })); - - -- Ok, we need to re-send any stanzas that the client didn't see - -- ...they are what is now left in the outgoing stanza queue - -- We have to use the send of "session" because we don't want to add our resent stanzas - -- to the outgoing queue again + -- Let everyone know that we are no longer hibernating + module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()}); + original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption + request_ack_now_if_needed(original_session, true, "handle_resume", nil); + resumption_age:sample(age); + end; + }; +end - session.log("debug", "resending all unacked stanzas that are still queued after resume, #queue = %d", queue:count_unacked()); - -- FIXME Which session is it that the queue filter sees? - session.resending_unacked = true; - original_session.resending_unacked = true; - for _, queued_stanza in queue:resume() do - session.send(queued_stanza); - end - session.resending_unacked = nil; - original_session.resending_unacked = nil; - session.log("debug", "all stanzas resent, now disabling send() in this migrated session, #queue = %d", queue:count_unacked()); - function session.send(stanza) -- luacheck: ignore 432 - migrated_session_log("error", "Tried to send stanza on old session migrated by smacks resume (maybe there is a bug?): %s", tostring(stanza)); - return false; - end - module:fire_event("smacks-hibernation-end", {origin = session, resumed = original_session, queue = queue:table()}); - original_session.awaiting_ack = nil; -- Don't wait for acks from before the resumption - request_ack_now_if_needed(original_session, true, "handle_resume", nil); - resumption_age:sample(age); +function handle_resume(session, stanza, xmlns_sm) + local resumed, err = do_resume(session, stanza); + if not resumed then + session.send(st.stanza("failed", { xmlns = xmlns_sm, h = format_h(err.context.h) }) + :tag(err.condition, { xmlns = xmlns_errors })); + return true; end + + session = resumed.session; + + -- Inform client of successful resumption + session.send(st.stanza("resumed", { xmlns = xmlns_sm, + h = format_h(session.handled_stanza_count), previd = resumed.id })); + + -- Complete resume (sync stanzas, etc.) + resumed.finish(); + return true; end + module:hook_tag(xmlns_sm2, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm2); end); module:hook_tag(xmlns_sm3, "resume", function (session, stanza) return handle_resume(session, stanza, xmlns_sm3); end); @@ -712,8 +735,7 @@ for _, user in pairs(local_sessions) do for _, session in pairs(user.sessions) do if session.resumption_token then - if old_session_registry:set(session.username, session.resumption_token, - { h = session.handled_stanza_count; t = os.time() }) then + if save_old_session(session) then session.resumption_token = nil; -- Deal with unacked stanzas diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_stanza_debug.lua --- a/plugins/mod_stanza_debug.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_stanza_debug.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,6 @@ module:set_global(); -local filters = require "util.filters"; +local filters = require "prosody.util.filters"; local function log_send(t, session) if t and t ~= "" and t ~= " " then diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_storage_internal.lua --- a/plugins/mod_storage_internal.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_storage_internal.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,17 +1,20 @@ -local cache = require "util.cache"; -local datamanager = require "core.storagemanager".olddm; -local array = require "util.array"; -local datetime = require "util.datetime"; -local st = require "util.stanza"; -local now = require "util.time".now; -local id = require "util.id".medium; -local jid_join = require "util.jid".join; -local set = require "util.set"; +local cache = require "prosody.util.cache"; +local datamanager = require "prosody.core.storagemanager".olddm; +local array = require "prosody.util.array"; +local datetime = require "prosody.util.datetime"; +local st = require "prosody.util.stanza"; +local now = require "prosody.util.time".now; +local id = require "prosody.util.id".medium; +local jid_join = require "prosody.util.jid".join; +local set = require "prosody.util.set"; +local it = require "prosody.util.iterators"; local host = module.host; -local archive_item_limit = module:get_option_number("storage_archive_item_limit", 10000); -local archive_item_count_cache = cache.new(module:get_option("storage_archive_item_limit_cache_size", 1000)); +local archive_item_limit = module:get_option_integer("storage_archive_item_limit", 10000, 0); +local archive_item_count_cache = cache.new(module:get_option_integer("storage_archive_item_limit_cache_size", 1000, 1)); + +local use_shift = module:get_option_boolean("storage_archive_experimental_fast_delete", false); local driver = {}; @@ -121,100 +124,144 @@ return key; end +local function binary_search(haystack, test, min, max) + if min == nil then + min = 1; + end + if max == nil then + max = #haystack; + end + + local floor = math.floor; + while min < max do + local mid = floor((max + min) / 2); + + local result = test(haystack[mid]); + if result < 0 then + max = mid; + elseif result > 0 then + min = mid + 1; + else + return mid, haystack[mid]; + end + end + + return min, nil; +end + function archive:find(username, query) - local items, err = datamanager.list_load(username, host, self.store); - if not items then + local list, err = datamanager.list_open(username, host, self.store); + if not list then if err then - return items, err; + return list, err; elseif query then if query.before or query.after then return nil, "item-not-found"; end if query.total then - return function () end, 0; + return function() + end, 0; end end - return function () end; + return function() + end; end - local count = nil; - local i, last_key = 0; + + local i = 0; + local iter = function() + i = i + 1; + return list[i] + end + if query then - items = array(items); + if query.reverse then + i = #list + 1 + iter = function() + i = i - 1 + return list[i] + end + query.before, query.after = query.after, query.before; + end if query.key then - items:filter(function (item) + iter = it.filter(function(item) return item.key == query.key; - end); + end, iter); end if query.ids then local ids = set.new(query.ids); - items:filter(function (item) + iter = it.filter(function(item) return ids:contains(item.key); - end); + end, iter); end if query.with then - items:filter(function (item) + iter = it.filter(function(item) return item.with == query.with; - end); + end, iter); end if query.start then - items:filter(function (item) - local when = item.when or datetime.parse(item.attr.stamp); - return when >= query.start; - end); + if not query.reverse then + local wi = binary_search(list, function(item) + local when = item.when or datetime.parse(item.attr.stamp); + return query.start - when; + end); + i = wi - 1; + else + iter = it.filter(function(item) + local when = item.when or datetime.parse(item.attr.stamp); + return when >= query.start; + end, iter); + end end if query["end"] then - items:filter(function (item) - local when = item.when or datetime.parse(item.attr.stamp); - return when <= query["end"]; - end); - end - if query.total then - count = #items; - end - if query.reverse then - items:reverse(); - if query.before then - local found = false; - for j = 1, #items do - if (items[j].key or tostring(j)) == query.before then - found = true; - i = j; - break; - end + if query.reverse then + local wi = binary_search(list, function(item) + local when = item.when or datetime.parse(item.attr.stamp); + return query["end"] - when; + end); + if wi then + i = wi + 1; end - if not found then - return nil, "item-not-found"; - end + else + iter = it.filter(function(item) + local when = item.when or datetime.parse(item.attr.stamp); + return when <= query["end"]; + end, iter); end - last_key = query.after; - elseif query.after then + end + if query.after then local found = false; - for j = 1, #items do - if (items[j].key or tostring(j)) == query.after then - found = true; - i = j; - break; + iter = it.filter(function(item) + local found_after = found; + if item.key == query.after then + found = true end - end - if not found then - return nil, "item-not-found"; - end - last_key = query.before; - elseif query.before then - last_key = query.before; + return found_after; + end, iter); end - if query.limit and #items - i > query.limit then - items[i+query.limit+1] = nil; + if query.before then + local found = false; + iter = it.filter(function(item) + if item.key == query.before then + found = true + end + return not found; + end, iter); + end + if query.limit then + iter = it.head(query.limit, iter); end end - return function () - i = i + 1; - local item = items[i]; - if not item or (last_key and item.key == last_key) then - return; + + return function() + local item = iter(); + if item == nil then + if list.close then + list:close(); + end + return end - local key = item.key or tostring(i); - local when = item.when or datetime.parse(item.attr.stamp); + local key = item.key; + local when = item.when or item.attr and datetime.parse(item.attr.stamp); local with = item.with; item.key, item.when, item.with = nil, nil, nil; item.attr.stamp = nil; @@ -222,7 +269,7 @@ item.attr.stamp_legacy = nil; item = st.deserialize(item); return key, item, when, with; - end, count; + end end function archive:get(username, wanted_key) @@ -297,12 +344,53 @@ return datamanager.users(host, self.store, "list"); end +function archive:trim(username, to_when) + local cache_key = jid_join(username, host, self.store); + local list, err = datamanager.list_open(username, host, self.store); + if not list then + if err == nil then + module:log("debug", "store already empty, can't trim"); + return 0; + end + return list, err; + end + + -- shortcut: check if the last item should be trimmed, if so, drop the whole archive + local last = list[#list].when or datetime.parse(list[#list].attr.stamp); + if last <= to_when then + if list.close then + list:close() + end + return datamanager.list_store(username, host, self.store, nil); + end + + -- luacheck: ignore 211/exact + local i, exact = binary_search(list, function(item) + local when = item.when or datetime.parse(item.attr.stamp); + return to_when - when; + end); + if list.close then + list:close() + end + -- TODO if exact then ... off by one? + if i == 1 then return 0; end + local ok, err = datamanager.list_shift(username, host, self.store, i); + if not ok then return ok, err; end + archive_item_count_cache:set(cache_key, nil); -- TODO calculate how many items are left + return i-1; +end + function archive:delete(username, query) local cache_key = jid_join(username, host, self.store); if not query or next(query) == nil then - archive_item_count_cache:set(cache_key, nil); + archive_item_count_cache:set(cache_key, nil); -- nil because we don't check if the following succeeds return datamanager.list_store(username, host, self.store, nil); end + + if use_shift and next(query) == "end" and next(query, "end") == nil then + return self:trim(username, query["end"]); + end + local items, err = datamanager.list_load(username, host, self.store); if not items then if err then diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_storage_memory.lua --- a/plugins/mod_storage_memory.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_storage_memory.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,15 +1,15 @@ -local serialize = require "util.serialization".serialize; -local array = require "util.array"; -local envload = require "util.envload".envload; -local st = require "util.stanza"; +local serialize = require "prosody.util.serialization".serialize; +local array = require "prosody.util.array"; +local envload = require "prosody.util.envload".envload; +local st = require "prosody.util.stanza"; local is_stanza = st.is_stanza or function (s) return getmetatable(s) == st.stanza_mt end -local new_id = require "util.id".medium; -local set = require "util.set"; +local new_id = require "prosody.util.id".medium; +local set = require "prosody.util.set"; local auto_purge_enabled = module:get_option_boolean("storage_memory_temporary", false); local auto_purge_stores = module:get_option_set("storage_memory_temporary_stores", {}); -local archive_item_limit = module:get_option_number("storage_archive_item_limit", 1000); +local archive_item_limit = module:get_option_integer("storage_archive_item_limit", 1000, 0); local memory = setmetatable({}, { __index = function(t, k) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_storage_sql.lua --- a/plugins/mod_storage_sql.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_storage_sql.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,19 +1,34 @@ -- luacheck: ignore 212/self -local cache = require "util.cache"; -local json = require "util.json"; -local sql = require "util.sql"; -local xml_parse = require "util.xml".parse; -local uuid = require "util.uuid"; -local resolve_relative_path = require "util.paths".resolve_relative_path; -local jid_join = require "util.jid".join; +local cache = require "prosody.util.cache"; +local json = require "prosody.util.json"; +local xml_parse = require "prosody.util.xml".parse; +local uuid = require "prosody.util.uuid"; +local resolve_relative_path = require "prosody.util.paths".resolve_relative_path; +local jid_join = require "prosody.util.jid".join; -local is_stanza = require"util.stanza".is_stanza; +local is_stanza = require"prosody.util.stanza".is_stanza; local t_concat = table.concat; +local have_dbisql, dbisql = pcall(require, "prosody.util.sql"); +local have_sqlite, sqlite = pcall(require, "prosody.util.sqlite3"); +if not have_dbisql then + module:log("debug", "Could not load LuaDBI, error was: %s", dbisql) + dbisql = nil; +end +if not have_sqlite then + module:log("debug", "Could not load LuaSQLite3, error was: %s", sqlite) + sqlite = nil; +end +if not (have_dbisql or have_sqlite) then + module:log("error", "LuaDBI or LuaSQLite3 are required for using SQL databases but neither are installed"); + module:log("error", "Please install at least one of LuaDBI and LuaSQLite3. See https://prosody.im/doc/depends"); + error("No SQL library available") +end + local noop = function() end -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; local function iterator(result) return function(result_) local row = result_(); @@ -59,9 +74,8 @@ end local host = module.host; -local user, store; -local function keyval_store_get() +local function keyval_store_get(user, store) local haveany; local result = {}; local select_sql = [[ @@ -86,7 +100,7 @@ return result; end end -local function keyval_store_set(data) +local function keyval_store_set(data, user, store) local delete_sql = [[ DELETE FROM "prosody" WHERE "host"=? AND "user"=? AND "store"=? @@ -121,19 +135,15 @@ local keyval_store = {}; keyval_store.__index = keyval_store; function keyval_store:get(username) - user, store = username, self.store; - local ok, result = engine:transaction(keyval_store_get); + local ok, result = engine:transaction(keyval_store_get, username, self.store); if not ok then - module:log("error", "Unable to read from database %s store for %s: %s", store, username or "", result); + module:log("error", "Unable to read from database %s store for %s: %s", self.store, username or "", result); return nil, result; end return result; end function keyval_store:set(username, data) - user,store = username,self.store; - return engine:transaction(function() - return keyval_store_set(data); - end); + return engine:transaction(keyval_store_set, data, username, self.store); end function keyval_store:users() local ok, result = engine:transaction(function() @@ -150,8 +160,8 @@ --- Archive store API -local archive_item_limit = module:get_option_number("storage_archive_item_limit"); -local archive_item_count_cache = cache.new(module:get_option("storage_archive_item_limit_cache_size", 1000)); +local archive_item_limit = module:get_option_integer("storage_archive_item_limit", nil, 0); +local archive_item_count_cache = cache.new(module:get_option_integer("storage_archive_item_limit_cache_size", 1000, 1)); local item_count_cache_hit = module:measure("item_count_cache_hit", "rate"); local item_count_cache_miss = module:measure("item_count_cache_miss", "rate") @@ -201,6 +211,13 @@ ("host","user","store","key","type","value") VALUES (?,?,?,?,?,?); ]]; + local upsert_sql = [[ + INSERT INTO "prosody" + ("host","user","store","key","type","value") + VALUES (?,?,?,?,?,?) + ON CONFLICT ("host", "user","store", "key") + DO UPDATE SET "type"=?, "value"=?; + ]]; local select_extradata_sql = [[ SELECT "type", "value" FROM "prosody" @@ -208,7 +225,10 @@ LIMIT 1; ]]; for key, data in pairs(keydatas) do - if type(key) == "string" and key ~= "" then + if type(key) == "string" and key ~= "" and engine.params.driver ~= "MySQL" and data ~= self.remove then + local t, value = assert(serialize(data)); + engine:insert(upsert_sql, host, username or "", self.store, key, t, value, t, value); + elseif type(key) == "string" and key ~= "" then engine:delete(delete_sql, host, username or "", self.store, key); if data ~= self.remove then @@ -291,37 +311,43 @@ local user,store = username,self.store; local cache_key = jid_join(username, host, store); local item_count = archive_item_count_cache:get(cache_key); - if not item_count then - item_count_cache_miss(); - local ok, ret = engine:transaction(function() - local count_sql = [[ - SELECT COUNT(*) FROM "prosodyarchive" - WHERE "host"=? AND "user"=? AND "store"=?; - ]]; - local result = engine:select(count_sql, host, user, store); - if result then - for row in result do - item_count = row[1]; - end - end - end); - if not ok or not item_count then - module:log("error", "Failed while checking quota for %s: %s", username, ret); - return nil, "Failure while checking quota"; - end - archive_item_count_cache:set(cache_key, item_count); - else - item_count_cache_hit(); - end if archive_item_limit then + if not item_count then + item_count_cache_miss(); + local ok, ret = engine:transaction(function() + local count_sql = [[ + SELECT COUNT(*) FROM "prosodyarchive" + WHERE "host"=? AND "user"=? AND "store"=?; + ]]; + local result = engine:select(count_sql, host, user, store); + if result then + for row in result do + item_count = row[1]; + end + end + end); + if not ok or not item_count then + module:log("error", "Failed while checking quota for %s: %s", username, ret); + return nil, "Failure while checking quota"; + end + archive_item_count_cache:set(cache_key, item_count); + else + item_count_cache_hit(); + end + module:log("debug", "%s has %d items out of %d limit", username, item_count, archive_item_limit); if item_count >= archive_item_limit then return nil, "quota-limit"; end end + -- FIXME update the schema to allow precision timestamps when = when or os.time(); + if engine.params.driver ~= "SQLite3" then + -- SQLite3 doesn't enforce types :) + when = math.floor(when); + end with = with or ""; local ok, ret = engine:transaction(function() local delete_sql = [[ @@ -334,16 +360,19 @@ VALUES (?,?,?,?,?,?,?,?); ]]; if key then + -- TODO use UPSERT like map store local result = engine:delete(delete_sql, host, user or "", store, key); - if result then + if result and item_count then item_count = item_count - result:affected(); end else - key = uuid.generate(); + key = uuid.v7(); end local t, encoded_value = assert(serialize(value)); engine:insert(insert_sql, host, user or "", store, when, with, key, t, encoded_value); - archive_item_count_cache:set(cache_key, item_count+1); + if item_count then + archive_item_count_cache:set(cache_key, item_count+1); + end return key; end); if not ok then return ok, ret; end @@ -354,12 +383,12 @@ local function archive_where(query, args, where) -- Time range, inclusive if query.start then - args[#args+1] = query.start + args[#args+1] = math.floor(query.start); where[#where+1] = "\"when\" >= ?" end if query["end"] then - args[#args+1] = query["end"]; + args[#args+1] = math.floor(query["end"]); if query.start then where[#where] = "\"when\" BETWEEN ? AND ?" -- is this inclusive? else @@ -382,8 +411,7 @@ -- Set of ids if query.ids then local nids, nargs = #query.ids, #args; - -- COMPAT Lua 5.1: No separator argument to string.rep - where[#where + 1] = "\"key\" IN (" .. string.rep("?,", nids):sub(1,-2) .. ")"; + where[#where + 1] = "\"key\" IN (" .. string.rep("?", nids, ",") .. ")"; for i, id in ipairs(query.ids) do args[nargs+i] = id; end @@ -611,7 +639,7 @@ LIMIT %s OFFSET ? );]]; if engine.params.driver == "SQLite3" then - if engine._have_delete_limit then + if engine.sqlite_compile_options.enable_update_delete_limit then sql_query = [[ DELETE FROM "prosodyarchive" WHERE %s @@ -630,7 +658,13 @@ archive_item_count_cache:clear(); else local cache_key = jid_join(username, host, self.store); - archive_item_count_cache:set(cache_key, nil); + if query.start == nil and query.with == nil and query["end"] == nil and query.key == nil and query.ids == nil and query.truncate == nil then + -- All items deleted, count should be zero. + archive_item_count_cache:set(cache_key, 0); + else + -- Not sure how many items left + archive_item_count_cache:set(cache_key, nil); + end end return ok and stmt:affected(), stmt; end @@ -648,10 +682,27 @@ return iterator(result); end +local keyvalplus = { + __index = { + -- keyval + get = keyval_store.get; + set = keyval_store.set; + items = keyval_store.users; + -- map + get_key = map_store.get; + set_key = map_store.set; + remove = map_store.remove; + set_keys = map_store.set_keys; + get_key_from_all = map_store.get_all; + delete_key_from_all = map_store.delete_all; + }; +} + local stores = { keyval = keyval_store; map = map_store; archive = archive_store; + ["keyval+"] = keyvalplus; }; --- Implement storage driver API @@ -692,6 +743,7 @@ local function create_table(engine) -- luacheck: ignore 431/engine + local sql = engine.params.driver == "SQLite3" and sqlite or dbisql; local Table, Column, Index = sql.Table, sql.Column, sql.Index; local ProsodyTable = Table { @@ -702,7 +754,7 @@ Column { name="key", type="TEXT", nullable=false }; Column { name="type", type="TEXT", nullable=false }; Column { name="value", type="MEDIUMTEXT", nullable=false }; - Index { name="prosody_index", "host", "user", "store", "key" }; + Index { name = "prosody_unique_index"; unique = engine.params.driver ~= "MySQL"; "host"; "user"; "store"; "key" }; }; engine:transaction(function() ProsodyTable:create(engine); @@ -732,6 +784,7 @@ local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore 431/engine local changes = false; if params.driver == "MySQL" then + local sql = dbisql; local success,err = engine:transaction(function() do local result = assert(engine:execute("SHOW COLUMNS FROM \"prosody\" WHERE \"Field\"='value' and \"Type\"='text'")); @@ -799,12 +852,38 @@ success,err = engine:transaction(function() return engine:execute(check_encoding_query, params.database, engine.charset, engine.charset.."_bin"); - end); - if not success then - module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error"); - return false; + end); + if not success then + module:log("error", "Failed to check/upgrade database encoding: %s", err or "unknown error"); + return false; + end + else + local indices = {}; + engine:transaction(function () + if params.driver == "SQLite3" then + for row in engine:select [[SELECT "name" FROM "sqlite_schema" WHERE "type"='index' AND "tbl_name"='prosody' AND "name"='prosody_index';]] do + indices[row[1]] = true; + end + elseif params.driver == "PostgreSQL" then + for row in engine:select [[SELECT "indexname" FROM "pg_indexes" WHERE "tablename"='prosody' AND "indexname"='prosody_index';]] do + indices[row[1]] = true; + end + end + end) + if indices["prosody_index"] then + if apply_changes then + local success = engine:transaction(function () + return assert(engine:execute([[DROP INDEX "prosody_index";]])); + end); + if not success then + module:log("error", "Failed to delete obsolete index \"prosody_index\""); + return false; + end + else + changes = true; + end + end end - end return changes; end @@ -831,12 +910,13 @@ function module.load() local engines = module:shared("/*/sql/connections"); local params = normalize_params(module:get_option("sql", default_params)); + local sql = params.driver == "SQLite3" and sqlite or dbisql; local db_uri = sql.db2uri(params); engine = engines[db_uri]; if not engine then module:log("debug", "Creating new engine %s", db_uri); engine = sql:create_engine(params, function (engine) -- luacheck: ignore 431/engine - if module:get_option("sql_manage_tables", true) then + if module:get_option_boolean("sql_manage_tables", true) then -- Automatically create table, ignore failure (table probably already exists) -- FIXME: we should check in information_schema, etc. create_table(engine); @@ -847,28 +927,74 @@ end end if engine.params.driver == "SQLite3" then + local compile_options = {} for row in engine:select("PRAGMA compile_options") do - if row[1] == "ENABLE_UPDATE_DELETE_LIMIT" then - engine._have_delete_limit = true; + local option = row[1]:lower(); + local opt, val = option:match("^([^=]+)=(.*)$"); + compile_options[opt or option] = tonumber(val) or val or true; + end + engine.sqlite_compile_options = compile_options; + + local journal_mode = "delete"; + for row in engine:select[[PRAGMA journal_mode;]] do + journal_mode = row[1]; + end + + -- Note: These things can't be changed with in a transaction. LuaDBI + -- opens a transaction automatically for every statement(?), so this + -- will not work there. + local tune = module:get_option_enum("sqlite_tune", "default", "normal", "fast", "safe"); + if tune == "normal" then + if journal_mode ~= "wal" then + engine:execute("PRAGMA journal_mode=WAL;"); end + engine:execute("PRAGMA auto_vacuum=FULL;"); + engine:execute("PRAGMA synchronous=NORMAL;") + elseif tune == "fast" then + if journal_mode ~= "wal" then + engine:execute("PRAGMA journal_mode=WAL;"); + end + if compile_options.secure_delete then + engine:execute("PRAGMA secure_delete=FAST;"); + end + engine:execute("PRAGMA synchronous=OFF;") + engine:execute("PRAGMA fullfsync=0;") + elseif tune == "safe" then + if journal_mode ~= "delete" then + engine:execute("PRAGMA journal_mode=DELETE;"); + end + engine:execute("PRAGMA synchronous=EXTRA;") + engine:execute("PRAGMA fullfsync=1;") end + + for row in engine:select[[PRAGMA journal_mode;]] do + journal_mode = row[1]; + end + + module:log("debug", "SQLite3 database %q operating with journal_mode=%s", engine.params.database, journal_mode); end + module:set_status("info", "Connected to " .. engine.params.driver); + end, function (engine) -- luacheck: ignore 431/engine + module:set_status("error", "Disconnected from " .. engine.params.driver); end); engines[sql.db2uri(params)] = engine; + else + module:set_status("info", "Using existing engine"); end module:provides("storage", driver); end function module.command(arg) - local config = require "core.configmanager"; - local hi = require "util.human.io"; + local config = require "prosody.core.configmanager"; + local hi = require "prosody.util.human.io"; local command = table.remove(arg, 1); if command == "upgrade" then -- We need to find every unique dburi in the config local uris = {}; for host in pairs(prosody.hosts) do -- luacheck: ignore 431/host local params = normalize_params(config.get(host, "sql") or default_params); + local sql = engine.params.driver == "SQLite3" and sqlite or dbisql; uris[sql.db2uri(params)] = params; end print("We will check and upgrade the following databases:\n"); @@ -884,6 +1010,7 @@ -- Upgrade each one for _, params in pairs(uris) do print("Checking "..params.database.."..."); + local sql = params.driver == "SQLite3" and sqlite or dbisql; engine = sql:create_engine(params); upgrade_table(engine, params, true); end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_storage_xep0227.lua --- a/plugins/mod_storage_xep0227.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_storage_xep0227.lua Wed Mar 27 15:39:03 2024 +0000 @@ -2,22 +2,22 @@ local ipairs, pairs = ipairs, pairs; local setmetatable = setmetatable; local tostring = tostring; -local next, unpack = next, table.unpack or unpack; --luacheck: ignore 113/unpack +local next, unpack = next, table.unpack; local os_remove = os.remove; local io_open = io.open; -local jid_bare = require "util.jid".bare; -local jid_prep = require "util.jid".prep; -local jid_join = require "util.jid".join; +local jid_bare = require "prosody.util.jid".bare; +local jid_prep = require "prosody.util.jid".prep; +local jid_join = require "prosody.util.jid".join; -local array = require "util.array"; -local base64 = require "util.encodings".base64; -local dt = require "util.datetime"; -local hex = require "util.hex"; -local it = require "util.iterators"; -local paths = require"util.paths"; -local set = require "util.set"; -local st = require "util.stanza"; -local parse_xml_real = require "util.xml".parse; +local array = require "prosody.util.array"; +local base64 = require "prosody.util.encodings".base64; +local dt = require "prosody.util.datetime"; +local hex = require "prosody.util.hex"; +local it = require "prosody.util.iterators"; +local paths = require"prosody.util.paths"; +local set = require "prosody.util.set"; +local st = require "prosody.util.stanza"; +local parse_xml_real = require "prosody.util.xml".parse; local lfs = require "lfs"; @@ -80,7 +80,7 @@ -- In order to support custom account properties local extended = "http://prosody.im/protocol/extended-xep0227\1"; -local scram_hash_name = module:get_option_string("password_hash", "SHA-1"); +local scram_hash_name = module:get_option_enum("password_hash", "SHA-1", "SHA-256"); local scram_properties = set.new({ "server_key", "stored_key", "iteration_count", "salt" }); handlers.accounts = { diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_time.lua --- a/plugins/mod_time.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_time.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,9 +6,9 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; -local datetime = require "util.datetime".datetime; -local legacy = require "util.datetime".legacy; +local st = require "prosody.util.stanza"; +local datetime = require "prosody.util.datetime".datetime; +local now = require "prosody.util.time".now; -- XEP-0202: Entity Time @@ -18,23 +18,10 @@ local origin, stanza = event.origin, event.stanza; origin.send(st.reply(stanza):tag("time", {xmlns="urn:xmpp:time"}) :tag("tzo"):text("+00:00"):up() -- TODO get the timezone in a platform independent fashion - :tag("utc"):text(datetime())); + :tag("utc"):text(datetime(now()))); return true; end module:hook("iq-get/bare/urn:xmpp:time:time", time_handler); module:hook("iq-get/host/urn:xmpp:time:time", time_handler); --- XEP-0090: Entity Time (deprecated) - -module:add_feature("jabber:iq:time"); - -local function legacy_time_handler(event) - local origin, stanza = event.origin, event.stanza; - origin.send(st.reply(stanza):tag("query", {xmlns="jabber:iq:time"}) - :tag("utc"):text(legacy())); - return true; -end - -module:hook("iq-get/bare/jabber:iq:time:query", legacy_time_handler); -module:hook("iq-get/host/jabber:iq:time:query", legacy_time_handler); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_tls.lua --- a/plugins/mod_tls.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_tls.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,14 +6,14 @@ -- COPYING file in the source package for more information. -- -local create_context = require "core.certmanager".create_context; -local rawgetopt = require"core.configmanager".rawget; -local st = require "util.stanza"; +local create_context = require "prosody.core.certmanager".create_context; +local rawgetopt = require"prosody.core.configmanager".rawget; +local st = require "prosody.util.stanza"; -local c2s_require_encryption = module:get_option("c2s_require_encryption", module:get_option("require_encryption", true)); -local s2s_require_encryption = module:get_option("s2s_require_encryption", true); -local allow_s2s_tls = module:get_option("s2s_allow_encryption") ~= false; -local s2s_secure_auth = module:get_option("s2s_secure_auth"); +local c2s_require_encryption = module:get_option_boolean("c2s_require_encryption", module:get_option_boolean("require_encryption", true)); +local s2s_require_encryption = module:get_option_boolean("s2s_require_encryption", true); +local allow_s2s_tls = module:get_option_boolean("s2s_allow_encryption", true); +local s2s_secure_auth = module:get_option_boolean("s2s_secure_auth", false); if s2s_secure_auth and s2s_require_encryption == false then module:log("warn", "s2s_secure_auth implies s2s_require_encryption, but s2s_require_encryption is set to false"); @@ -62,7 +62,7 @@ module:log("debug", "Creating context for s2sout"); -- for outgoing server connections - ssl_ctx_s2sout, err_s2sout, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s, request_client_certs, xmpp_alpn); + ssl_ctx_s2sout, err_s2sout, ssl_cfg_s2sout = create_context(host.host, "client", host_s2s, host_ssl, global_s2s, xmpp_alpn); if not ssl_ctx_s2sout then module:log("error", "Error creating contexts for s2sout: %s", err_s2sout); end module:log("debug", "Creating context for s2sin"); @@ -80,6 +80,9 @@ module:hook_global("config-reloaded", module.load); local function can_do_tls(session) + if session.secure then + return false; + end if session.conn and not session.conn.starttls then if not session.secure then session.log("debug", "Underlying connection does not support STARTTLS"); @@ -125,7 +128,15 @@ -- Hook module:hook("stanza/urn:ietf:params:xml:ns:xmpp-tls:starttls", function(event) local origin = event.origin; + origin.starttls = "requested"; if can_do_tls(origin) then + if origin.conn.block_reads then + -- we need to ensure that no data is read anymore, otherwise we could end up in a situation where + -- is sent and the socket receives the TLS handshake (and passes the data to lua) before + -- it is asked to initiate TLS + -- (not with the classical single-threaded server backends) + origin.conn:block_reads() + end (origin.sends2s or origin.send)(starttls_proceed); if origin.destroyed then return end origin:reset_stream(); @@ -166,6 +177,7 @@ module:log("debug", "%s is not offering TLS", session.to_host); return; end + session.starttls = "initiated"; session.sends2s(starttls_initiate); return true; end @@ -183,7 +195,8 @@ if session.type == "s2sout_unauthed" and can_do_tls(session) then module:log("debug", "Proceeding with TLS on s2sout..."); session:reset_stream(); - session.conn:starttls(session.ssl_ctx); + session.starttls = "proceeding" + session.conn:starttls(session.ssl_ctx, session.to_host); session.secure = false; return true; end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_tokenauth.lua --- a/plugins/mod_tokenauth.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_tokenauth.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,82 +1,354 @@ -local id = require "util.id"; -local jid = require "util.jid"; -local base64 = require "util.encodings".base64; +local base64 = require "prosody.util.encodings".base64; +local hashes = require "prosody.util.hashes"; +local id = require "prosody.util.id"; +local jid = require "prosody.util.jid"; +local random = require "prosody.util.random"; +local usermanager = require "prosody.core.usermanager"; +local generate_identifier = require "prosody.util.id".short; + +local token_store = module:open_store("auth_tokens", "keyval+"); + +local access_time_granularity = module:get_option_period("token_auth_access_time_granularity", 60); +local empty_grant_lifetime = module:get_option_period("tokenless_grant_ttl", "2w"); -local token_store = module:open_store("auth_tokens", "map"); +local function select_role(username, host, role_name) + if not role_name then return end + local role = usermanager.get_role_by_name(role_name, host); + if not role then return end + if not usermanager.user_can_assume_role(username, host, role.name) then return end + return role; +end -function create_jid_token(actor_jid, token_jid, token_scope, token_ttl) - token_jid = jid.prep(token_jid); - if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then +function create_grant(actor_jid, grant_jid, grant_ttl, grant_data) + grant_jid = jid.prep(grant_jid); + if not actor_jid or actor_jid ~= grant_jid and not jid.compare(grant_jid, actor_jid) then + module:log("debug", "Actor <%s> is not permitted to create a token granting access to JID <%s>", actor_jid, grant_jid); return nil, "not-authorized"; end - local token_username, token_host, token_resource = jid.split(token_jid); + local grant_username, grant_host, grant_resource = jid.split(grant_jid); + + if grant_host ~= module.host then + return nil, "invalid-host"; + end + + local grant_id = id.short(); + local now = os.time(); + + local grant = { + id = grant_id; + + owner = actor_jid; + created = now; + expires = grant_ttl and (now + grant_ttl) or nil; + accessed = now; + + jid = grant_jid; + resource = grant_resource; + + data = grant_data; + + -- tokens[..":"..] = token_info + tokens = {}; + }; + + local ok, err = token_store:set_key(grant_username, grant_id, grant); + if not ok then + return nil, err; + end + + module:fire_event("token-grant-created", { + id = grant_id; + grant = grant; + username = grant_username; + host = grant_host; + }); + + return grant; +end + +function create_token(grant_jid, grant, token_role, token_ttl, token_purpose, token_data) + if (token_data and type(token_data) ~= "table") or (token_purpose and type(token_purpose) ~= "string") then + return nil, "bad-request"; + end + local grant_username, grant_host = jid.split(grant_jid); + if grant_host ~= module.host then + return nil, "invalid-host"; + end + if type(grant) == "string" then -- lookup by id + grant = token_store:get_key(grant_username, grant); + if not grant then return nil; end + end + + if not grant.tokens then return nil, "internal-server-error"; end -- old-style token? + + local now = os.time(); + local expires = grant.expires; -- Default to same expiry as grant + if token_ttl then -- explicit lifetime requested + if expires then + -- Grant has an expiry, so limit to that or shorter + expires = math.min(now + token_ttl, expires); + else + -- Grant never expires, just use whatever expiry is requested for the token + expires = now + token_ttl; + end + end + + local token_info = { + role = token_role; + + created = now; + expires = expires; + purpose = token_purpose; + + data = token_data; + }; + + local token_secret = random.bytes(18); + grant.tokens["sha256:"..hashes.sha256(token_secret, true)] = token_info; + local ok, err = token_store:set_key(grant_username, grant.id, grant); + if not ok then + return nil, err; + end + + local token_string = "secret-token:"..base64.encode("2;"..grant.id..";"..token_secret..";"..grant.jid); + return token_string, token_info; +end + +local function parse_token(encoded_token) + if not encoded_token then return nil; end + local encoded_data = encoded_token:match("^secret%-token:(.+)$"); + if not encoded_data then return nil; end + local token = base64.decode(encoded_data); + if not token then return nil; end + local token_id, token_secret, token_jid = token:match("^2;([^;]+);(..................);(.+)$"); + if not token_id then return nil; end + local token_user, token_host = jid.split(token_jid); + return token_id, token_user, token_host, token_secret; +end + +local function clear_expired_grant_tokens(grant, now) + local updated; + now = now or os.time(); + for secret, token_info in pairs(grant.tokens) do + local expires = token_info.expires; + if expires and expires < now then + grant.tokens[secret] = nil; + updated = true; + end + end + return updated; +end + +local function _get_validated_grant_info(username, grant) + if type(grant) == "string" then + grant = token_store:get_key(username, grant); + end + if not grant or not grant.created or not grant.id then return nil; end + + -- Invalidate grants from before last password change + local account_info = usermanager.get_account_info(username, module.host); + local password_updated_at = account_info and account_info.password_updated; + local now = os.time(); + if password_updated_at and grant.created < password_updated_at then + module:log("debug", "Token grant %s of %s issued before last password change, invalidating it now", grant.id, username); + token_store:set_key(username, grant.id, nil); + return nil, "not-authorized"; + elseif grant.expires and grant.expires < now then + module:log("debug", "Token grant %s of %s expired, cleaning up", grant.id, username); + token_store:set_key(username, grant.id, nil); + return nil, "expired"; + end + + if not grant.tokens then + module:log("debug", "Token grant %s of %s without tokens, cleaning up", grant.id, username); + token_store:set_key(username, grant.id, nil); + return nil, "invalid"; + end + + local found_expired = false + for secret_hash, token_info in pairs(grant.tokens) do + if token_info.expires and token_info.expires < now then + module:log("debug", "Token %s of grant %s of %s has expired, cleaning it up", secret_hash:sub(-8), grant.id, username); + grant.tokens[secret_hash] = nil; + found_expired = true; + end + end + + if not grant.expires and next(grant.tokens) == nil and grant.accessed + empty_grant_lifetime < now then + module:log("debug", "Token %s of %s grant has no tokens, discarding", grant.id, username); + token_store:set_key(username, grant.id, nil); + return nil, "expired"; + elseif found_expired then + token_store:set_key(username, grant.id, grant); + end + + return grant; +end + +local function _get_validated_token_info(token_id, token_user, token_host, token_secret) if token_host ~= module.host then return nil, "invalid-host"; end - local token_info = { - owner = actor_jid; - created = os.time(); - expires = token_ttl and (os.time() + token_ttl) or nil; - jid = token_jid; - session = { - username = token_username; - host = token_host; - resource = token_resource; + local grant, err = token_store:get_key(token_user, token_id); + if not grant or not grant.tokens then + if err then + module:log("error", "Unable to read from token storage: %s", err); + return nil, "internal-error"; + end + module:log("warn", "Invalid token in storage (%s / %s)", token_user, token_id); + return nil, "not-authorized"; + end + + -- Check provided secret + local secret_hash = "sha256:"..hashes.sha256(token_secret, true); + local token_info = grant.tokens[secret_hash]; + if not token_info then + module:log("debug", "No tokens matched the given secret"); + return nil, "not-authorized"; + end - auth_scope = token_scope; - }; - }; + -- Check expiry + local now = os.time(); + if token_info.expires and token_info.expires < now then + module:log("debug", "Token has expired, cleaning it up"); + grant.tokens[secret_hash] = nil; + token_store:set_key(token_user, token_id, grant); + return nil, "not-authorized"; + end - local token_id = id.long(); - local token = base64.encode("1;"..jid.join(token_username, token_host)..";"..token_id); - token_store:set(token_username, token_id, token_info); + -- Verify grant validity (expiry, etc.) + grant = _get_validated_grant_info(token_user, grant); + if not grant then + return nil, "not-authorized"; + end - return token, token_info; + -- Update last access time if necessary + local last_accessed = grant.accessed; + if not last_accessed or (now - last_accessed) > access_time_granularity then + grant.accessed = now; + clear_expired_grant_tokens(grant); -- Clear expired tokens while we're here + token_store:set_key(token_user, token_id, grant); + end + + token_info.id = token_id; + token_info.grant = grant; + token_info.jid = grant.jid; + + return token_info; end -local function parse_token(encoded_token) - local token = base64.decode(encoded_token); - if not token then return nil; end - local token_jid, token_id = token:match("^1;([^;]+);(.+)$"); - if not token_jid then return nil; end - local token_user, token_host = jid.split(token_jid); - return token_id, token_user, token_host; +function get_grant_info(username, grant_id) + local grant = _get_validated_grant_info(username, grant_id); + if not grant then return nil; end + + -- Caller is only interested in the grant, no need to expose token stuff to them + grant.tokens = nil; + + return grant; +end + +function get_user_grants(username) + local grants = token_store:get(username); + if not grants then return nil; end + for grant_id, grant in pairs(grants) do + grants[grant_id] = _get_validated_grant_info(username, grant); + end + return grants; end function get_token_info(token) - local token_id, token_user, token_host = parse_token(token); + local token_id, token_user, token_host, token_secret = parse_token(token); + if not token_id then + module:log("warn", "Failed to verify access token: %s", token_user); + return nil, "invalid-token-format"; + end + return _get_validated_token_info(token_id, token_user, token_host, token_secret); +end + +function get_token_session(token, resource) + local token_id, token_user, token_host, token_secret = parse_token(token); if not token_id then + module:log("warn", "Failed to verify access token: %s", token_user); + return nil, "invalid-token-format"; + end + + local token_info, err = _get_validated_token_info(token_id, token_user, token_host, token_secret); + if not token_info then return nil, err; end + + local role = select_role(token_user, token_host, token_info.role); + if not role then return nil, "not-authorized"; end + return { + username = token_user; + host = token_host; + resource = token_info.resource or resource or generate_identifier(); + + role = role; + }; +end + +function revoke_token(token) + local grant_id, token_user, token_host, token_secret = parse_token(token); + if not grant_id then + module:log("warn", "Failed to verify access token: %s", token_user); return nil, "invalid-token-format"; end if token_host ~= module.host then return nil, "invalid-host"; end - - local token_info, err = token_store:get(token_user, token_id); - if not token_info then - if err then - return nil, "internal-error"; - end - return nil, "not-authorized"; + local grant, err = _get_validated_grant_info(token_user, grant_id); + if not grant then return grant, err; end + local secret_hash = "sha256:"..hashes.sha256(token_secret, true); + local token_info = grant.tokens[secret_hash]; + if not grant or not token_info then + return nil, "item-not-found"; end - - if token_info.expires and token_info.expires < os.time() then - return nil, "not-authorized"; + grant.tokens[secret_hash] = nil; + local ok, err = token_store:set_key(token_user, grant_id, grant); + if not ok then + return nil, err; end - - return token_info + module:fire_event("token-revoked", { + grant_id = grant_id; + grant = grant; + info = token_info; + username = token_user; + host = token_host; + }); + return true; end -function revoke_token(token) - local token_id, token_user, token_host = parse_token(token); - if not token_id then - return nil, "invalid-token-format"; +function revoke_grant(username, grant_id) + local ok, err = token_store:set_key(username, grant_id, nil); + if not ok then return nil, err; end + module:fire_event("token-grant-revoked", { id = grant_id, username = username, host = module.host }); + return true; +end + +function sasl_handler(auth_provider, purpose, extra) + return function (sasl, token, realm, _authzid) + local token_info, err = get_token_info(token); + if not token_info then + module:log("debug", "SASL handler failed to verify token: %s", err); + return nil, nil, extra; + end + local token_user, token_host, resource = jid.split(token_info.grant.jid); + if realm ~= token_host or (purpose and token_info.purpose ~= purpose) then + return nil, nil, extra; + end + if auth_provider.is_enabled and not auth_provider.is_enabled(token_user) then + return true, false, token_info; + end + sasl.resource = resource; + sasl.token_info = token_info; + return token_user, true, token_info; + end; +end + +module:daily("clear expired grants", function() + for username in token_store:items() do + get_user_grants(username); -- clears out expired grants end - if token_host ~= module.host then - return nil, "invalid-host"; - end - return token_store:set(token_user, token_id, nil); -end +end) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_tombstones.lua --- a/plugins/mod_tombstones.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_tombstones.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,16 +1,16 @@ -- TODO warn when trying to create an user before the tombstone expires -- e.g. via telnet or other admin interface -local datetime = require "util.datetime"; -local errors = require "util.error"; -local jid_node = require"util.jid".node; -local st = require "util.stanza"; +local datetime = require "prosody.util.datetime"; +local errors = require "prosody.util.error"; +local jid_node = require"prosody.util.jid".node; +local st = require "prosody.util.stanza"; -- Using a map store as key-value store so that removal of all user data -- does not also remove the tombstone, which would defeat the point local graveyard = module:open_store(nil, "map"); -local graveyard_cache = require "util.cache".new(module:get_option_number("tombstone_cache_size", 1024)); +local graveyard_cache = require "prosody.util.cache".new(module:get_option_integer("tombstone_cache_size", 1024, 1)); -local ttl = module:get_option_number("user_tombstone_expiry", nil); +local ttl = module:get_option_period("user_tombstone_expiry", nil); -- Keep tombstones forever by default -- -- Rationale: diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_turn_external.lua --- a/plugins/mod_turn_external.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_turn_external.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,12 +1,12 @@ -local set = require "util.set"; +local set = require "prosody.util.set"; local secret = module:get_option_string("turn_external_secret"); local host = module:get_option_string("turn_external_host", module.host); local user = module:get_option_string("turn_external_user"); -local port = module:get_option_number("turn_external_port", 3478); -local ttl = module:get_option_number("turn_external_ttl", 86400); +local port = module:get_option_integer("turn_external_port", 3478, 1, 65535); +local ttl = module:get_option_period("turn_external_ttl", "1 day"); local tcp = module:get_option_boolean("turn_external_tcp", false); -local tls_port = module:get_option_number("turn_external_tls_port"); +local tls_port = module:get_option_integer("turn_external_tls_port", nil, 1, 65535); if not secret then module:log_status("error", "Failed to initialize: the 'turn_external_secret' option is not set in your configuration"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_uptime.lua --- a/plugins/mod_uptime.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_uptime.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,7 +6,7 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local start_time = prosody.start_time; module:hook_global("server-started", function() start_time = prosody.start_time end); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_user_account_management.lua --- a/plugins/mod_user_account_management.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_user_account_management.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,16 +7,28 @@ -- -local st = require "util.stanza"; -local usermanager_set_password = require "core.usermanager".set_password; -local usermanager_delete_user = require "core.usermanager".delete_user; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local jid_bare = require "util.jid".bare; +local st = require "prosody.util.stanza"; +local usermanager = require "prosody.core.usermanager"; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local jid_bare, jid_node = import("prosody.util.jid", "bare", "node"); local compat = module:get_option_boolean("registration_compat", true); +local soft_delete_period = module:get_option_period("registration_delete_grace_period"); +local deleted_accounts = module:open_store("accounts_cleanup"); module:add_feature("jabber:iq:register"); +-- Allow us to 'freeze' a session and retrieve properties even after it is +-- destroyed +local function capture_session_properties(session) + return setmetatable({ + id = session.id; + ip = session.ip; + type = session.type; + client_id = session.client_id; + }, { __index = session }); +end + -- Password change and account deletion handler local function handle_registration_stanza(event) local session, stanza = event.origin, event.stanza; @@ -34,6 +46,12 @@ if query.tags[1] and query.tags[1].name == "remove" then local username, host = session.username, session.host; + if host ~= module.host then -- Sanity check for safety + module:log("error", "Host mismatch on deletion request (a bug): %s ~= %s", host, module.host); + session.send(st.error_reply(stanza, "cancel", "internal-server-error")); + return true; + end + -- This one weird trick sends a reply to this stanza before the user is deleted local old_session_close = session.close; session.close = function(self, ...) @@ -41,24 +59,57 @@ return old_session_close(self, ...); end - local ok, err = usermanager_delete_user(username, host); + local old_session = capture_session_properties(session); + + if not soft_delete_period then + local ok, err = usermanager.delete_user(username, host); + + if not ok then + log("debug", "Removing user account %s@%s failed: %s", username, host, err); + session.close = old_session_close; + session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); + return true; + end + + log("info", "User removed their account: %s@%s (deleted)", username, host); + module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = old_session }); + else + local ok, err = usermanager.disable_user(username, host, { + reason = "ibr"; + comment = "Deletion requested by user"; + when = os.time(); + }); - if not ok then - log("debug", "Removing user account %s@%s failed: %s", username, host, err); - session.close = old_session_close; - session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); - return true; + if not ok then + log("debug", "Removing (disabling) user account %s@%s failed: %s", username, host, err); + session.close = old_session_close; + session.send(st.error_reply(stanza, "cancel", "service-unavailable", err)); + return true; + end + + local status = { + deleted_at = os.time(); + pending_until = os.time() + soft_delete_period; + client_id = session.client_id; + }; + deleted_accounts:set(username, status); + + log("info", "User removed their account: %s@%s (disabled, pending deletion)", username, host); + module:fire_event("user-deregistered-pending", { + username = username; + host = host; + source = "mod_register"; + session = old_session; + status = status; + }); end - - log("info", "User removed their account: %s@%s", username, host); - module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); else local username = query:get_child_text("username"); local password = query:get_child_text("password"); if username and password then username = nodeprep(username); if username == session.username then - if usermanager_set_password(username, password, session.host, session.resource) then + if usermanager.set_password(username, password, session.host, session.resource) then session.send(st.reply(stanza)); else -- TODO unable to write file, file may be locked, etc, what's the correct error? @@ -85,3 +136,103 @@ end); end +-- This improves UX of soft-deleted accounts by informing the user that the +-- account has been deleted, rather than just disabled. They can e.g. contact +-- their admin if this was a mistake. +module:hook("authentication-failure", function (event) + if event.condition ~= "account-disabled" then return; end + local session = event.session; + local sasl_handler = session and session.sasl_handler; + if sasl_handler.username then + local status = deleted_accounts:get(sasl_handler.username); + if status then + event.text = "Account deleted"; + end + end +end, -1000); + +function restore_account(username) + local pending, pending_err = deleted_accounts:get(username); + if not pending then + return nil, pending_err or "Account not pending deletion"; + end + local account_info, err = usermanager.get_account_info(username, module.host); + if not account_info then + return nil, "Couldn't fetch account info: "..err; + end + local forget_ok, forget_err = deleted_accounts:set(username, nil); + if not forget_ok then + return nil, "Couldn't remove account from deletion queue: "..forget_err; + end + local enable_ok, enable_err = usermanager.enable_user(username, module.host); + if not enable_ok then + return nil, "Removed account from deletion queue, but couldn't enable it: "..enable_err; + end + return true, "Account restored"; +end + +-- Automatically clear pending deletion if an account is re-enabled +module:context("*"):hook("user-enabled", function (event) + if event.host ~= module.host then return; end + deleted_accounts:set(event.username, nil); +end); + +local cleanup_time = module:measure("cleanup", "times"); + +function cleanup_soft_deleted_accounts() + local cleanup_done = cleanup_time(); + local success, fail, restored, pending = 0, 0, 0, 0; + + for username in deleted_accounts:users() do + module:log("debug", "Processing account cleanup for '%s'", username); + local account_info, account_info_err = usermanager.get_account_info(username, module.host); + if not account_info then + module:log("warn", "Unable to process delayed deletion of user '%s': %s", username, account_info_err); + fail = fail + 1; + else + if account_info.enabled == false then + local meta = deleted_accounts:get(username); + if meta.pending_until <= os.time() then + local ok, err = usermanager.delete_user(username, module.host); + if not ok then + module:log("warn", "Unable to process delayed deletion of user '%s': %s", username, err); + fail = fail + 1; + else + success = success + 1; + deleted_accounts:set(username, nil); + module:log("debug", "Deleted account '%s' successfully", username); + module:fire_event("user-deregistered", { username = username, host = module.host, source = "mod_register" }); + end + else + pending = pending + 1; + end + else + module:log("warn", "Account '%s' is not disabled, removing from deletion queue", username); + restored = restored + 1; + end + end + end + + module:log("debug", "%d accounts scheduled for future deletion", pending); + + if success > 0 or fail > 0 then + module:log("info", "Completed account cleanup - %d accounts deleted (%d failed, %d restored, %d pending)", success, fail, restored, pending); + end + cleanup_done(); +end + +module:daily("Remove deleted accounts", cleanup_soft_deleted_accounts); + +--- shell command +module:add_item("shell-command", { + section = "user"; + name = "restore"; + desc = "Restore a user account scheduled for deletion"; + args = { + { name = "jid", type = "string" }; + }; + host_selector = "jid"; + handler = function (self, jid) --luacheck: ignore 212/self + return restore_account(jid_node(jid)); + end; +}); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_vcard.lua --- a/plugins/mod_vcard.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_vcard.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,8 +6,8 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza" -local jid_split = require "util.jid".split; +local st = require "prosody.util.stanza" +local jid_split = require "prosody.util.jid".split; local vcards = module:open_store(); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_vcard4.lua --- a/plugins/mod_vcard4.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_vcard4.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,5 +1,5 @@ -local st = require "util.stanza" -local jid_split = require "util.jid".split; +local st = require "prosody.util.stanza" +local jid_split = require "prosody.util.jid".split; local mod_pep = module:depends("pep"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_vcard_legacy.lua --- a/plugins/mod_vcard_legacy.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_vcard_legacy.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,10 @@ -local st = require "util.stanza"; -local jid_split = require "util.jid".split; +local st = require "prosody.util.stanza"; +local jid_split = require "prosody.util.jid".split; local mod_pep = module:depends("pep"); -local sha1 = require "util.hashes".sha1; -local base64_decode = require "util.encodings".base64.decode; +local sha1 = require "prosody.util.hashes".sha1; +local base64_decode = require "prosody.util.encodings".base64.decode; local vcards = module:open_store("vcard"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_version.lua --- a/plugins/mod_version.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_version.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,7 +6,7 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; module:add_feature("jabber:iq:version"); @@ -20,7 +20,7 @@ platform = "Windows"; else local os_version_command = module:get_option_string("os_version_command"); - local ok, pposix = pcall(require, "util.pposix"); + local ok, pposix = pcall(require, "prosody.util.pposix"); if not os_version_command and (ok and pposix and pposix.uname) then platform = pposix.uname().sysname; end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_watchregistrations.lua --- a/plugins/mod_watchregistrations.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_watchregistrations.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,14 +8,14 @@ local host = module:get_host(); -local jid_prep = require "util.jid".prep; +local jid_prep = require "prosody.util.jid".prep; local registration_watchers = module:get_option_set("registration_watchers", module:get_option("admins", {})) / jid_prep; local registration_from = module:get_option_string("registration_from", host); local registration_notification = module:get_option_string("registration_notification", "User $username just registered on $host from $ip"); -local msg_type = module:get_option_string("registration_notification_type", "chat"); +local msg_type = module:get_option_enum("registration_notification_type", "chat", "normal", "headline"); -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; module:hook("user-registered", function (user) module:log("debug", "Notifying of new registration"); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_websocket.lua --- a/plugins/mod_websocket.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_websocket.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,19 +8,19 @@ module:set_global(); -local add_task = require "util.timer".add_task; -local add_filter = require "util.filters".add_filter; -local sha1 = require "util.hashes".sha1; -local base64 = require "util.encodings".base64.encode; -local st = require "util.stanza"; -local parse_xml = require "util.xml".parse; -local contains_token = require "util.http".contains_token; -local portmanager = require "core.portmanager"; -local sm_destroy_session = require"core.sessionmanager".destroy_session; +local add_task = require "prosody.util.timer".add_task; +local add_filter = require "prosody.util.filters".add_filter; +local sha1 = require "prosody.util.hashes".sha1; +local base64 = require "prosody.util.encodings".base64.encode; +local st = require "prosody.util.stanza"; +local parse_xml = require "prosody.util.xml".parse; +local contains_token = require "prosody.util.http".contains_token; +local portmanager = require "prosody.core.portmanager"; +local sm_destroy_session = require"prosody.core.sessionmanager".destroy_session; local log = module._log; -local dbuffer = require "util.dbuffer"; +local dbuffer = require "prosody.util.dbuffer"; -local websocket_frames = require"net.websocket.frames"; +local websocket_frames = require"prosody.net.websocket.frames"; local parse_frame = websocket_frames.parse; local build_frame = websocket_frames.build; local build_close = websocket_frames.build_close; @@ -28,10 +28,10 @@ local t_concat = table.concat; -local stanza_size_limit = module:get_option_number("c2s_stanza_size_limit", 1024 * 256); -local frame_buffer_limit = module:get_option_number("websocket_frame_buffer_limit", 2 * stanza_size_limit); -local frame_fragment_limit = module:get_option_number("websocket_frame_fragment_limit", 8); -local stream_close_timeout = module:get_option_number("c2s_close_timeout", 5); +local stanza_size_limit = module:get_option_integer("c2s_stanza_size_limit", 1024 * 256, 10000); +local frame_buffer_limit = module:get_option_integer("websocket_frame_buffer_limit", 2 * stanza_size_limit, 0); +local frame_fragment_limit = module:get_option_integer("websocket_frame_fragment_limit", 8, 0); +local stream_close_timeout = module:get_option_period("c2s_close_timeout", 5); local consider_websocket_secure = module:get_option_boolean("consider_websocket_secure"); local cross_domain = module:get_option("cross_domain_websocket"); if cross_domain ~= nil then @@ -370,6 +370,6 @@ module:hook("c2s-read-timeout", keepalive, -0.9); end -if require"core.modulemanager".get_modules_for_host("*"):contains(module.name) then +if require"prosody.core.modulemanager".get_modules_for_host("*"):contains(module.name) then module:add_host(); end diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/mod_welcome.lua --- a/plugins/mod_welcome.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/mod_welcome.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,7 +9,7 @@ local host = module:get_host(); local welcome_text = module:get_option_string("welcome_message", "Hello $username, welcome to the $host IM server!"); -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; module:hook("user-registered", function (user) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/hats.lib.lua --- a/plugins/muc/hats.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/hats.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,4 @@ -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local muc_util = module:require "muc/util"; local xmlns_hats = "xmpp:prosody.im/protocol/hats:1"; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/hidden.lib.lua --- a/plugins/muc/hidden.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/hidden.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,7 +8,7 @@ -- local restrict_public = not module:get_option_boolean("muc_room_allow_public", true); -local um_is_admin = require "core.usermanager".is_admin; +module:default_permission(restrict_public and "prosody:admin" or "prosody:registered", ":create-public-room"); local function get_hidden(room) return room._data.hidden; @@ -22,8 +22,8 @@ end module:hook("muc-config-form", function(event) - if restrict_public and not um_is_admin(event.actor, module.host) then - -- Don't show option if public rooms are restricted and user is not admin of this host + if not module:may(":create-public-room", event.actor) then + -- Hide config option if this user is not allowed to create public rooms return; end table.insert(event.form, { @@ -36,7 +36,7 @@ end, 100-9); module:hook("muc-config-submitted/muc#roomconfig_publicroom", function(event) - if restrict_public and not um_is_admin(event.actor, module.host) then + if not module:may(":create-public-room", event.actor) then return; -- Not allowed end if set_hidden(event.room, not event.value) then diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/history.lib.lua --- a/plugins/muc/history.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/history.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,11 +8,11 @@ -- local gettime = os.time; -local datetime = require "util.datetime"; -local st = require "util.stanza"; +local datetime = require "prosody.util.datetime"; +local st = require "prosody.util.stanza"; local default_history_length = 20; -local max_history_length = module:get_option_number("max_history_messages", math.huge); +local max_history_length = module:get_option_integer("max_history_messages", math.huge, 0); local function set_max_history_length(_max_history_length) max_history_length = _max_history_length or math.huge; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/lock.lib.lua --- a/plugins/muc/lock.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/lock.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,10 +7,10 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local lock_rooms = module:get_option_boolean("muc_room_locking", true); -local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300); +local lock_room_timeout = module:get_option_period("muc_room_lock_timeout", "5 minutes"); local function lock(room) module:fire_event("muc-room-locked", {room = room;}); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/members_only.lib.lua --- a/plugins/muc/members_only.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/members_only.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,7 +7,7 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local muc_util = module:require "muc/util"; local valid_affiliations = muc_util.valid_affiliations; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/mod_muc.lua --- a/plugins/muc/mod_muc.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/mod_muc.lua Wed Mar 27 15:39:03 2024 +0000 @@ -89,18 +89,17 @@ local presence_broadcast = module:require "muc/presence_broadcast"; room_mt.get_presence_broadcast = presence_broadcast.get; room_mt.set_presence_broadcast = presence_broadcast.set; -room_mt.get_valid_broadcast_roles = presence_broadcast.get_valid_broadcast_roles; +room_mt.get_valid_broadcast_roles = presence_broadcast.get_valid_broadcast_roles; -- FIXME doesn't exist in the library local occupant_id = module:require "muc/occupant_id"; room_mt.get_salt = occupant_id.get_room_salt; room_mt.get_occupant_id = occupant_id.get_occupant_id; -local jid_split = require "util.jid".split; -local jid_prep = require "util.jid".prep; -local jid_bare = require "util.jid".bare; -local st = require "util.stanza"; -local cache = require "util.cache"; -local um_is_admin = require "core.usermanager".is_admin; +local jid_split = require "prosody.util.jid".split; +local jid_prep = require "prosody.util.jid".prep; +local jid_bare = require "prosody.util.jid".bare; +local st = require "prosody.util.stanza"; +local cache = require "prosody.util.cache"; module:require "muc/config_form_sections"; @@ -111,21 +110,26 @@ module:require "muc/hats"; module:require "muc/lock"; -local function is_admin(jid) - return um_is_admin(jid, module.host); -end +module:default_permissions("prosody:admin", { + ":automatic-ownership"; + ":create-room"; + ":recreate-destroyed-room"; +}); +module:default_permissions("prosody:guest", { + ":list-rooms"; +}); if module:get_option_boolean("component_admins_as_room_owners", true) then -- Monkey patch to make server admins room owners local _get_affiliation = room_mt.get_affiliation; function room_mt:get_affiliation(jid) - if is_admin(jid) then return "owner"; end + if module:could(":automatic-ownership", jid) then return "owner"; end return _get_affiliation(self, jid); end local _set_affiliation = room_mt.set_affiliation; function room_mt:set_affiliation(actor, jid, affiliation, reason, data) - if affiliation ~= "owner" and is_admin(jid) then return nil, "modify", "not-acceptable"; end + if affiliation ~= "owner" and module:could(":automatic-ownership", jid) then return nil, "modify", "not-acceptable"; end return _set_affiliation(self, actor, jid, affiliation, reason, data); end end @@ -158,8 +162,8 @@ end end -local max_rooms = module:get_option_number("muc_max_rooms"); -local max_live_rooms = module:get_option_number("muc_room_cache_size", 100); +local max_rooms = module:get_option_integer("muc_max_rooms", nil, 0); +local max_live_rooms = module:get_option_integer("muc_room_cache_size", 100, 1); local room_hit = module:measure("room_hit", "rate"); local room_miss = module:measure("room_miss", "rate") @@ -281,15 +285,14 @@ room:set_public(module:get_option_boolean("muc_room_default_public", false)); room:set_persistent(module:get_option_boolean("muc_room_default_persistent", room:get_persistent())); room:set_members_only(module:get_option_boolean("muc_room_default_members_only", room:get_members_only())); - room:set_allow_member_invites(module:get_option_boolean("muc_room_default_allow_member_invites", - room:get_allow_member_invites())); + room:set_allow_member_invites(module:get_option_boolean("muc_room_default_allow_member_invites", room:get_allow_member_invites())); room:set_moderated(module:get_option_boolean("muc_room_default_moderated", room:get_moderated())); - room:set_whois(module:get_option_boolean("muc_room_default_public_jids", - room:get_whois() == "anyone") and "anyone" or "moderators"); + room:set_whois(module:get_option_boolean("muc_room_default_public_jids", room:get_whois() == "anyone") and "anyone" or "moderators"); room:set_changesubject(module:get_option_boolean("muc_room_default_change_subject", room:get_changesubject())); - room:set_historylength(module:get_option_number("muc_room_default_history_length", room:get_historylength())); + room:set_historylength(module:get_option_integer("muc_room_default_history_length", room:get_historylength(), 0)); room:set_language(lang or module:get_option_string("muc_room_default_language")); - room:set_presence_broadcast(module:get_option("muc_room_default_presence_broadcast", room:get_presence_broadcast())); + room:set_presence_broadcast(module:get_option_enum("muc_room_default_presence_broadcast", room:get_presence_broadcast(), "visitor", "participant", + "moderator")); end function create_room(room_jid, config) @@ -350,8 +353,12 @@ end module:hook("host-disco-items", function(event) + module:log("debug", "host-disco-items called"); + if not module:could(":list-rooms", event) then + module:log("debug", "Returning empty room list to unauthorized request"); + return; + end local reply = event.reply; - module:log("debug", "host-disco-items called"); if next(room_items_cache) ~= nil then for jid, room_name in pairs(room_items_cache) do if room_name == "" then room_name = nil; end @@ -388,7 +395,7 @@ if module:get_option_boolean("muc_tombstones", true) then - local ttl = module:get_option_number("muc_tombstone_expiry", 86400 * 31); + local ttl = module:get_option_period("muc_tombstone_expiry", "31 days"); module:hook("muc-room-destroyed",function(event) local room = event.room; @@ -412,26 +419,15 @@ end, -10); end -do - local restrict_room_creation = module:get_option("restrict_room_creation"); - if restrict_room_creation == true then - restrict_room_creation = "admin"; +local restrict_room_creation = module:get_option_enum("restrict_room_creation", false, true, "local"); +module:default_permission(restrict_room_creation == true and "prosody:admin" or "prosody:registered", ":create-room"); +module:hook("muc-room-pre-create", function(event) + local origin, stanza = event.origin, event.stanza; + if restrict_room_creation ~= false and not module:may(":create-room", event) then + origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host)); + return true; end - if restrict_room_creation then - local host_suffix = module.host:gsub("^[^%.]+%.", ""); - module:hook("muc-room-pre-create", function(event) - local origin, stanza = event.origin, event.stanza; - local user_jid = stanza.attr.from; - if not is_admin(user_jid) and not ( - restrict_room_creation == "local" and - select(2, jid_split(user_jid)) == host_suffix - ) then - origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host)); - return true; - end - end); - end -end +end); for event_name, method in pairs { -- Normal room interactions @@ -465,7 +461,7 @@ if room and room._data.destroyed then if room._data.locked < os.time() - or (is_admin(stanza.attr.from) and stanza.name == "presence" and stanza.attr.type == nil) then + or (module:may(":recreate-destroyed-room", event) and stanza.name == "presence" and stanza.attr.type == nil) then -- Allow the room to be recreated by admin or after time has passed delete_room(room); room = nil; @@ -516,10 +512,10 @@ module:depends "adhoc"; local t_concat = table.concat; local adhoc_new = module:require "adhoc".new; - local adhoc_initial = require "util.adhoc".new_initial_data_form; - local adhoc_simple = require "util.adhoc".new_simple_form; - local array = require "util.array"; - local dataforms_new = require "util.dataforms".new; + local adhoc_initial = require "prosody.util.adhoc".new_initial_data_form; + local adhoc_simple = require "prosody.util.adhoc".new_simple_form; + local array = require "prosody.util.array"; + local dataforms_new = require "prosody.util.dataforms".new; local destroy_rooms_layout = dataforms_new { title = "Destroy rooms"; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/muc.lib.lua --- a/plugins/muc/muc.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/muc.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,18 +12,18 @@ local next = next; local setmetatable = setmetatable; -local dataform = require "util.dataforms"; -local iterators = require "util.iterators"; -local jid_split = require "util.jid".split; -local jid_bare = require "util.jid".bare; -local jid_prep = require "util.jid".prep; -local jid_join = require "util.jid".join; -local jid_resource = require "util.jid".resource; -local resourceprep = require "util.encodings".stringprep.resourceprep; -local st = require "util.stanza"; -local base64 = require "util.encodings".base64; -local hmac_sha256 = require "util.hashes".hmac_sha256; -local new_id = require "util.id".medium; +local dataform = require "prosody.util.dataforms"; +local iterators = require "prosody.util.iterators"; +local jid_split = require "prosody.util.jid".split; +local jid_bare = require "prosody.util.jid".bare; +local jid_prep = require "prosody.util.jid".prep; +local jid_join = require "prosody.util.jid".join; +local jid_resource = require "prosody.util.jid".resource; +local resourceprep = require "prosody.util.encodings".stringprep.resourceprep; +local st = require "prosody.util.stanza"; +local base64 = require "prosody.util.encodings".base64; +local hmac_sha256 = require "prosody.util.hashes".hmac_sha256; +local new_id = require "prosody.util.id".medium; local log = module._log; @@ -1079,7 +1079,10 @@ local reason = item:get_child_text("reason"); local success, errtype, err if item.attr.affiliation and item.attr.jid and not item.attr.role then - local registration_data; + local registration_data = self:get_affiliation_data(item.attr.jid) or {}; + if reason then + registration_data.reason = reason; + end if item.attr.nick then local room_nick = self.jid.."/"..item.attr.nick; local existing_occupant = self:get_occupant_by_nick(room_nick); @@ -1088,7 +1091,7 @@ self:set_role(true, room_nick, nil, "This nickname is reserved"); end module:log("debug", "Reserving %s for %s (%s)", item.attr.nick, item.attr.jid, item.attr.affiliation); - registration_data = { reserved_nickname = item.attr.nick }; + registration_data.reserved_nickname = item.attr.nick; end success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason, registration_data); elseif item.attr.role and item.attr.nick and not item.attr.affiliation then @@ -1119,9 +1122,13 @@ if (affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank) or (self:get_members_only() and self:get_whois() == "anyone" and affiliation_rank >= valid_affiliations.member) then local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin"); - for jid in self:each_affiliation(_aff or "none") do + for jid, _, data in self:each_affiliation(_aff or "none") do local nick = self:get_registered_nick(jid); - reply:tag("item", {affiliation = _aff, jid = jid, nick = nick }):up(); + reply:tag("item", {affiliation = _aff, jid = jid, nick = nick }); + if data and data.reason then + reply:text_tag("reason", data.reason); + end + reply:up(); end origin.send(reply:up()); return true; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/occupant.lib.lua --- a/plugins/muc/occupant.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/occupant.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,6 @@ local pairs = pairs; local setmetatable = setmetatable; -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local util = module:require "muc/util"; local function get_filtered_presence(stanza) diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/occupant_id.lib.lua --- a/plugins/muc/occupant_id.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/occupant_id.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -4,9 +4,9 @@ -- (C) 2020 Maxime “pep” Buquet -- (C) 2020 Matthew Wild -local uuid = require "util.uuid"; -local hmac_sha256 = require "util.hashes".hmac_sha256; -local b64encode = require "util.encodings".base64.encode; +local uuid = require "prosody.util.uuid"; +local hmac_sha256 = require "prosody.util.hashes".hmac_sha256; +local b64encode = require "prosody.util.encodings".base64.encode; local xmlns_occupant_id = "urn:xmpp:occupant-id:0"; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/password.lib.lua --- a/plugins/muc/password.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/password.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,7 +7,7 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local function get_password(room) return room._data.password; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/persistent.lib.lua --- a/plugins/muc/persistent.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/persistent.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,7 +8,10 @@ -- local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true); -local um_is_admin = require "core.usermanager".is_admin; +module:default_permission( + restrict_persistent and "prosody:admin" or "prosody:registered", + ":create-persistent-room" +); local function get_persistent(room) return room._data.persistent; @@ -22,8 +25,8 @@ end module:hook("muc-config-form", function(event) - if restrict_persistent and not um_is_admin(event.actor, module.host) then - -- Don't show option if hidden rooms are restricted and user is not admin of this host + if not module:may(":create-persistent-room", event.actor) then + -- Hide config option if this user is not allowed to create persistent rooms return; end table.insert(event.form, { @@ -36,7 +39,7 @@ end, 100-5); module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event) - if restrict_persistent and not um_is_admin(event.actor, module.host) then + if not module:may(":create-persistent-room", event.actor) then return; -- Not allowed end if set_persistent(event.room, event.value) then diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/presence_broadcast.lib.lua --- a/plugins/muc/presence_broadcast.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/presence_broadcast.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,7 +7,7 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local valid_roles = { "none", "visitor", "participant", "moderator" }; local default_broadcast = { diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/register.lib.lua --- a/plugins/muc/register.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/register.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,8 +1,8 @@ -local jid_bare = require "util.jid".bare; -local jid_resource = require "util.jid".resource; -local resourceprep = require "util.encodings".stringprep.resourceprep; -local st = require "util.stanza"; -local dataforms = require "util.dataforms"; +local jid_bare = require "prosody.util.jid".bare; +local jid_resource = require "prosody.util.jid".resource; +local resourceprep = require "prosody.util.encodings".stringprep.resourceprep; +local st = require "prosody.util.stanza"; +local dataforms = require "prosody.util.dataforms"; local allow_unaffiliated = module:get_option_boolean("allow_unaffiliated_register", false); @@ -94,8 +94,10 @@ local nick = get_registered_nick(room, jid_bare(stanza.attr.from)); if nick then if event.occupant then + -- someone is joining, force their nickname to the registered one event.occupant.nick = jid_bare(event.occupant.nick) .. "/" .. nick; elseif event.dest_occupant.nick ~= jid_bare(event.dest_occupant.nick) .. "/" .. nick then + -- someone is trying to change nickname to something other than their registered nickname, can't have that module:log("debug", "Attempt by %s to join as %s, but their reserved nick is %s", stanza.attr.from, requested_nick, nick); local reply = st.error_reply(stanza, "cancel", "not-acceptable", nil, room.jid):up(); origin.send(reply); diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/request.lib.lua --- a/plugins/muc/request.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/request.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,14 +7,14 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; -local jid_resource = require "util.jid".resource; +local st = require "prosody.util.stanza"; +local jid_resource = require "prosody.util.jid".resource; module:hook("muc-disco#info", function(event) event.reply:tag("feature", {var = "http://jabber.org/protocol/muc#request"}):up(); end); -local voice_request_form = require "util.dataforms".new({ +local voice_request_form = require "prosody.util.dataforms".new({ title = "Voice Request"; { name = "FORM_TYPE"; diff -r 54a936345aaa -r 5d9ec2e55d74 plugins/muc/subject.lib.lua --- a/plugins/muc/subject.lib.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/plugins/muc/subject.lib.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,8 +7,8 @@ -- COPYING file in the source package for more information. -- -local st = require "util.stanza"; -local dt = require "util.datetime"; +local st = require "prosody.util.stanza"; +local dt = require "prosody.util.datetime"; local muc_util = module:require "muc/util"; local valid_roles = muc_util.valid_roles; diff -r 54a936345aaa -r 5d9ec2e55d74 prosody --- a/prosody Wed Mar 27 15:35:15 2024 +0000 +++ b/prosody Wed Mar 27 15:39:03 2024 +0000 @@ -44,9 +44,19 @@ end +-- Check before first require, to preempt the probable failure +if _VERSION < "Lua 5.2" then + io.stderr:write("Prosody is no longer compatible with Lua 5.1\n") + io.stderr:write("See https://prosody.im/doc/depends#lua for more information\n") + return os.exit(1); +end -local startup = require "util.startup"; -local async = require "util.async"; +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end + +local startup = require "prosody.util.startup"; +local async = require "prosody.util.async"; -- Note: it's important that this thread is not GC'd, as some C libraries -- that are initialized here store a pointer to it ( :/ ). @@ -73,7 +83,7 @@ end local sleep = require"socket".sleep; - local server = require "net.server"; + local server = require "prosody.net.server"; while select(2, xpcall(server.loop, catch_uncaught_error)) ~= "quitting" do sleep(0.2); diff -r 54a936345aaa -r 5d9ec2e55d74 prosodyctl --- a/prosodyctl Wed Mar 27 15:35:15 2024 +0000 +++ b/prosodyctl Wed Mar 27 15:39:03 2024 +0000 @@ -44,22 +44,33 @@ ----------- -local startup = require "util.startup"; +-- Check before first require, to preempt the probable failure +if _VERSION < "Lua 5.2" then + io.stderr:write("Prosody is no longer compatible with Lua 5.1\n") + io.stderr:write("See https://prosody.im/doc/depends#lua for more information\n") + return os.exit(1); +end + +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end + +local startup = require "prosody.util.startup"; startup.prosodyctl(); ----------- -local configmanager = require "core.configmanager"; -local modulemanager = require "core.modulemanager" -local prosodyctl = require "util.prosodyctl" +local configmanager = require "prosody.core.configmanager"; +local modulemanager = require "prosody.core.modulemanager" +local prosodyctl = require "prosody.util.prosodyctl" local socket = require "socket" -local dependencies = require "util.dependencies"; +local dependencies = require "prosody.util.dependencies"; local lfs = dependencies.softreq "lfs"; ----------------------- -local parse_args = require "util.argparse".parse; -local human_io = require "util.human.io"; +local parse_args = require "prosody.util.argparse".parse; +local human_io = require "prosody.util.human.io"; local show_message, show_warning = prosodyctl.show_message, prosodyctl.show_warning; local show_usage = prosodyctl.show_usage; @@ -67,7 +78,7 @@ local call_luarocks = prosodyctl.call_luarocks; local error_messages = prosodyctl.error_messages; -local jid_split = require "util.jid".prepped_split; +local jid_split = require "prosody.util.jid".prepped_split; local prosodyctl_timeout = (configmanager.get("*", "prosodyctl_timeout") or 5) * 2; ----------------------- @@ -427,8 +438,8 @@ end local pwd = "."; - local sorted_pairs = require "util.iterators".sorted_pairs; - local hg = require"util.mercurial"; + local sorted_pairs = require "prosody.util.iterators".sorted_pairs; + local hg = require"prosody.util.mercurial"; local relpath = configmanager.resolve_relative_path; print("Prosody "..(prosody.version or "(unknown version)")); @@ -450,7 +461,7 @@ .."\n "; end))); print(""); - local have_pposix, pposix = pcall(require, "util.pposix"); + local have_pposix, pposix = pcall(require, "prosody.util.pposix"); if have_pposix and pposix.uname then print("# Operating system"); local uname, err = pposix.uname(); @@ -483,7 +494,7 @@ print(""); print("# Network"); print(""); - print("Backend: "..require "net.server".get_backend()); + print("Backend: "..require "prosody.net.server".get_backend()); print(""); print("# Lua module versions"); local module_versions, longest_name = {}, 8; @@ -505,7 +516,7 @@ } local lunbound = dependencies.softreq"lunbound"; local lxp = dependencies.softreq"lxp"; - local hashes = dependencies.softreq"util.hashes"; + local hashes = dependencies.softreq"prosody.util.hashes"; for name, module in pairs(package.loaded) do local version_field = alternate_version_fields[name] or "_VERSION"; if type(module) == "table" and rawget(module, version_field) @@ -538,7 +549,7 @@ end print(""); print("# library versions"); - if require "net.server".event_base then + if require "prosody.net.server".event_base then library_versions["libevent"] = require"luaevent".core.libevent_version(); end for name, version in sorted_pairs(library_versions) do @@ -554,6 +565,15 @@ return 0; end + if arg[1] and arg[1]:match"^mod_" then + -- TODO reword the usage text, document + local shell = require "prosody.util.prosodyctl.shell"; + arg[1] = arg[1]:match("^mod_(.*)"); -- strip mod_ prefix + table.insert(arg, 1, "module"); + table.insert(arg, 2, "reload"); + return shell.shell(arg); + end + service_command_warning("reload"); if not prosodyctl.isrunning() then @@ -573,7 +593,7 @@ end -- ejabberdctl compatibility -local unpack = table.unpack or unpack; -- luacheck: ignore 113 +local unpack = table.unpack; function commands.register(arg) local user, host, password = unpack(arg); @@ -628,8 +648,8 @@ --------------------- -local async = require "util.async"; -local server = require "net.server"; +local async = require "prosody.util.async"; +local server = require "prosody.net.server"; local watchers = { error = function (_, err) error(err); @@ -675,7 +695,7 @@ end if command and not commands[command] then - local ok, command_module = pcall(require, "util.prosodyctl."..command); + local ok, command_module = pcall(require, "prosody.util.prosodyctl."..command); if ok and command_module[command] then commands[command] = command_module[command]; end @@ -692,7 +712,7 @@ print(""); print("Where COMMAND may be one of:"); - local hidden_commands = require "util.set".new{ "register", "unregister" }; + local hidden_commands = require "prosody.util.set".new{ "register", "unregister" }; local commands_order = { "Process management:", "start"; "stop"; "restart"; "reload"; "status"; diff -r 54a936345aaa -r 5d9ec2e55d74 spec/core_moduleapi_spec.lua --- a/spec/core_moduleapi_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/core_moduleapi_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -11,8 +11,8 @@ local module = setmetatable({}, {__index = api}); local opt = nil; -function module:log() end -function module:get_option(name) +function module.log(_self) end +function module.get_option(_self, name) if name == "opt" then return opt; else @@ -20,7 +20,7 @@ end end -function test_option_value(value, returns) +local function test_option_value(value, returns) opt = value; assert(module:get_option_number("opt") == returns.number, "number doesn't match"); assert(module:get_option_string("opt") == returns.string, "string doesn't match"); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/core_storagemanager_spec.lua --- a/spec/core_storagemanager_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/core_storagemanager_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,13 +1,11 @@ -local unpack = table.unpack or unpack; -- luacheck: ignore 113 -local server = require "net.server_select"; -package.loaded["net.server"] = server; +local unpack = table.unpack; -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local function mock_prosody() _G.prosody = { core_post_stanza = function () end; - events = require "util.events".new(); + events = require "prosody.util.events".new(); hosts = {}; paths = { data = "./data"; @@ -36,6 +34,11 @@ }; }; +local test_only_driver = os.getenv "PROSODY_TEST_ONLY_STORAGE"; +if test_only_driver then + configs = { [test_only_driver] = configs[test_only_driver] } +end + local test_host = "storage-unit-tests.invalid"; describe("storagemanager", function () @@ -47,10 +50,10 @@ insulate(tagged_name.." #storage backend", function () mock_prosody(); - local config = require "core.configmanager"; - local sm = require "core.storagemanager"; - local hm = require "core.hostmanager"; - local mm = require "core.modulemanager"; + local config = require "prosody.core.configmanager"; + local sm = require "prosody.core.storagemanager"; + local hm = require "prosody.core.hostmanager"; + local mm = require "prosody.core.modulemanager"; -- Simple check to ensure insulation is working correctly assert.is_nil(config.get(test_host, "storage")); @@ -196,6 +199,136 @@ end); end); + describe("keyval+ stores", function () + -- These tests rely on being executed in order, disable any order + -- randomization for this block + randomize(false); + + local store, kv_store, map_store; + it("may be opened", function () + store = assert(sm.open(test_host, "test-kv+", "keyval+")); + end); + + local simple_data = { foo = "bar" }; + + it("may set data for a user", function () + assert(store:set("user9999", simple_data)); + end); + + it("may get data for a user", function () + assert.same(simple_data, assert(store:get("user9999"))); + end); + + it("may be opened as a keyval store", function () + kv_store = assert(sm.open(test_host, "test-kv+", "keyval")); + assert.same(simple_data, assert(kv_store:get("user9999"))); + end); + + it("may be opened as a map store", function () + map_store = assert(sm.open(test_host, "test-kv+", "map")); + assert.same("bar", assert(map_store:get("user9999", "foo"))); + end); + + it("may remove data for a user", function () + assert(store:set("user9999", nil)); + local ret, err = store:get("user9999"); + assert.is_nil(ret); + assert.is_nil(err); + end); + + + it("may set a specific key for a user", function () + assert(store:set_key("user9999", "foo", "bar")); + assert.same(kv_store:get("user9999"), { foo = "bar" }); + end); + + it("may get a specific key for a user", function () + assert.equal("bar", store:get_key("user9999", "foo")); + end); + + it("may find all users with a specific key", function () + assert.is_function(store.get_key_from_all); + assert(store:set_key("user9999b", "bar", "bar")); + assert(store:set_key("user9999c", "foo", "blah")); + local ret, err = store:get_key_from_all("foo"); + assert.is_nil(err); + assert.same({ user9999 = "bar", user9999c = "blah" }, ret); + end); + + it("rejects empty or non-string keys to get_all", function () + assert.is_function(store.get_key_from_all); + do + local ret, err = store:get_key_from_all(""); + assert.is_nil(ret); + assert.is_not_nil(err); + end + do + local ret, err = store:get_key_from_all(true); + assert.is_nil(ret); + assert.is_not_nil(err); + end + end); + + it("rejects empty or non-string keys to delete_all", function () + assert.is_function(store.delete_key_from_all); + do + local ret, err = store:delete_key_from_all(""); + assert.is_nil(ret); + assert.is_not_nil(err); + end + do + local ret, err = store:delete_key_from_all(true); + assert.is_nil(ret); + assert.is_not_nil(err); + end + end); + + it("may delete all instances of a specific key", function () + assert.is_function(store.delete_key_from_all); + assert(store:set_key("user9999b", "foo", "hello")); + + assert(store:delete_key_from_all("bar")); + -- Ensure key was deleted + do + local ret, err = store:get_key("user9999b", "bar"); + assert.is_nil(ret); + assert.is_nil(err); + end + -- Ensure other users/keys are intact + do + local ret, err = store:get_key("user9999", "foo"); + assert.equal("bar", ret); + assert.is_nil(err); + end + do + local ret, err = store:get_key("user9999b", "foo"); + assert.equal("hello", ret); + assert.is_nil(err); + end + do + local ret, err = store:get_key("user9999c", "foo"); + assert.equal("blah", ret); + assert.is_nil(err); + end + end); + + it("may remove data for a specific key for a user", function () + assert(store:set_key("user9999", "foo", nil)); + do + local ret, err = store:get_key("user9999", "foo"); + assert.is_nil(ret); + assert.is_nil(err); + end + + assert(store:set_key("user9999b", "foo", nil)); + do + local ret, err = store:get_key("user9999b", "foo"); + assert.is_nil(ret); + assert.is_nil(err); + end + end); + end); + describe("archive stores", function () randomize(false); @@ -211,13 +344,13 @@ local test_time = 1539204123; local test_data = { - { nil, test_stanza, test_time, "contact@example.com" }; - { nil, test_stanza, test_time+1, "contact2@example.com" }; - { nil, test_stanza, test_time+2, "contact2@example.com" }; + { nil, test_stanza, test_time-3, "contact@example.com" }; + { nil, test_stanza, test_time-2, "contact2@example.com" }; { nil, test_stanza, test_time-1, "contact2@example.com" }; - { nil, test_stanza, test_time-1, "contact3@example.com" }; - { nil, test_stanza, test_time+0, "contact3@example.com" }; + { nil, test_stanza, test_time+0, "contact2@example.com" }; { nil, test_stanza, test_time+1, "contact3@example.com" }; + { nil, test_stanza, test_time+2, "contact3@example.com" }; + { nil, test_stanza, test_time+3, "contact3@example.com" }; }; it("can be added to", function () @@ -260,7 +393,7 @@ assert.equal("test", item.name); assert.equal("urn:example:foo", item.attr.xmlns); assert.equal(2, #item.tags); - assert.equal(test_time, when); + assert.equal(test_time-3, when); end assert.equal(1, count); end); @@ -298,16 +431,16 @@ assert.equal("test", item.name); assert.equal("urn:example:foo", item.attr.xmlns); assert.equal(2, #item.tags); - assert(test_time <= when); + assert(when >= test_time, ("%d >= %d"):format(when, test_time)); end - assert.equal(#test_data - 2, count); + assert.equal(#test_data - 3, count); end); it("by time (start+end)", function () -- luacheck: ignore 211/err local data, err = archive:find("user", { - ["start"] = test_time; - ["end"] = test_time+1; + ["start"] = test_time-1; + ["end"] = test_time+2; }); assert.truthy(data); local count = 0; @@ -318,8 +451,8 @@ assert.equal("test", item.name); assert.equal("urn:example:foo", item.attr.xmlns); assert.equal(2, #item.tags); - assert(when >= test_time, ("%d >= %d"):format(when, test_time)); - assert(when <= test_time+1, ("%d <= %d"):format(when, test_time+1)); + assert(when >= test_time-1, ("%d >= %d"):format(when, test_time)); + assert(when <= test_time+2, ("%d <= %d"):format(when, test_time+1)); end assert.equal(4, count); end); @@ -427,6 +560,30 @@ end); + -- This tests combines the reverse flag with 'before' and 'after' to + -- ensure behaviour remains correct + it("by id (before and after) in reverse #full_id_range", function () + assert.truthy(archive.caps and archive.caps.full_id_range, "full ID range support") + local data, err = archive:find("user", { + ["after"] = test_data[1][1]; + ["before"] = test_data[4][1]; + reverse = true; + }); + assert.truthy(data, err); + local count = 0; + for id, item in data do + count = count + 1; + assert.truthy(id); + assert.equal(test_data[4-count][1], id); + assert(st.is_stanza(item)); + assert.equal("test", item.name); + assert.equal("urn:example:foo", item.attr.xmlns); + assert.equal(2, #item.tags); + end + assert.equal(2, count); + end); + + end); @@ -466,7 +623,7 @@ local data, err = archive:find("user", { with = "contact@example.com"; }); - assert.truthy(data); + assert.truthy(data, err); local count = 0; for id, item, when in data do -- luacheck: ignore id item when count = count + 1; diff -r 54a936345aaa -r 5d9ec2e55d74 spec/inputs/test_keys.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/inputs/test_keys.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,179 @@ +local test_keys = { + -- ECDSA keypair from jwt.io + ecdsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2 +OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r +1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G +-----END PRIVATE KEY----- +]]; + + ecdsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9 +q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg== +-----END PUBLIC KEY----- +]]; + + -- Self-generated ECDSA keypair + alt_ecdsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQnn4AHz2Zy+JMAgp +AZfKAm9F3s6791PstPf5XjHtETKhRANCAAScv9jI3+BOXXlCOXwmQYosIbl9mf4V +uOwfIoCYSLylAghyxO0n2of8Kji+D+4C1zxNKmZIQa4s8neaIIzXnMY1 +-----END PRIVATE KEY----- +]]; + + alt_ecdsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnL/YyN/gTl15Qjl8JkGKLCG5fZn+ +FbjsHyKAmEi8pQIIcsTtJ9qH/Co4vg/uAtc8TSpmSEGuLPJ3miCM15zGNQ== +-----END PUBLIC KEY----- +]]; + + -- JWT reference keys for ES512 + + ecdsa_521_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBgc4HZz+/fBbC7lmEww0AO3NK9wVZ +PDZ0VEnsaUFLEYpTzb90nITtJUcPUbvOsdZIZ1Q8fnbquAYgxXL5UgHMoywAib47 +6MkyyYgPk0BXZq3mq4zImTRNuaU9slj9TVJ3ScT3L1bXwVuPJDzpr5GOFpaj+WwM +Al8G7CqwoJOsW7Kddns= +-----END PUBLIC KEY----- +]]; + + ecdsa_521_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiyAa7aRHFDCh2qga +9sTUGINE5jHAFnmM8xWeT/uni5I4tNqhV5Xx0pDrmCV9mbroFtfEa0XVfKuMAxxf +Z6LM/yKhgYkDgYYABAGBzgdnP798FsLuWYTDDQA7c0r3BVk8NnRUSexpQUsRilPN +v3SchO0lRw9Ru86x1khnVDx+duq4BiDFcvlSAcyjLACJvjvoyTLJiA+TQFdmrear +jMiZNE25pT2yWP1NUndJxPcvVtfBW48kPOmvkY4WlqP5bAwCXwbsKrCgk6xbsp12 +ew== +-----END PRIVATE KEY----- +]]; + + -- Self-generated keys for ES512 + + alt_ecdsa_521_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBIxV0ecG/+qFc/kVPKs8Z6tjJEuRe +dzrEaqABY6THu7BhCjEoxPr6iRYdiFPzNruFORsCAKf/NFLSoCqyrw9S0YMA1xc+ +uW01145oxT7Sp8BOH1MyOh7xNh+LFLi6X4lV6j5GQrM1sKSa3O5m0+VJmLy5b7cy +oxNCzXrnEByz+EO2nYI= +-----END PUBLIC KEY----- +]]; + + alt_ecdsa_521_private_pem = [[ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIAV2XJQ4/5Pa5m43/AJdL4XzrRV/l7eQ1JObqmI95YDs3zxM5Mfygz +DivhvuPdZCZUR+TdZQEdYN4LpllCzrDwmTCgBwYFK4EEACOhgYkDgYYABAEjFXR5 +wb/6oVz+RU8qzxnq2MkS5F53OsRqoAFjpMe7sGEKMSjE+vqJFh2IU/M2u4U5GwIA +p/80UtKgKrKvD1LRgwDXFz65bTXXjmjFPtKnwE4fUzI6HvE2H4sUuLpfiVXqPkZC +szWwpJrc7mbT5UmYvLlvtzKjE0LNeucQHLP4Q7adgg== +-----END EC PRIVATE KEY----- +]]; + + -- Self-generated EdDSA (Ed25519) keypair + eddsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIOmrajEfnqdzdJzkJ4irQMCGbYRqrl0RlwPHIw+a5b7M +-----END PRIVATE KEY----- +]]; + + eddsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAFipbSXeGvPVK7eA4+hIOdutZTUUyXswVSbMGi0j1QKE= +-----END PUBLIC KEY----- +]]; + + -- RSA keypair from jwt.io + rsa_private_pem = [[ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj +MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu +NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ +qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg +p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR +ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi +VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV +laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8 +sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H +mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY +dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw +ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ +DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T +N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t +0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv +t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU +AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk +48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL +DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK +xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA +mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh +2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz +et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr +VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD +TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc +dn/RsYEONbwQSjIfMPkvxF+8HQ== +-----END PRIVATE KEY----- +]]; + + rsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo +4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u ++qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh +kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ +0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg +cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc +mwIDAQAB +-----END PUBLIC KEY----- +]]; + + + -- Self-generated RSA keypair + alt_rsa_private_pem = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA4bt6kor2TomqRXfjCFe6T42ibatloyHntZCUdlDDAkUh4oJ/ +4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq3XLDsjhyN4stxEi0UVAiqqBkcEnk +qbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R2s5AHcOhdFYKeDuitqHT5r+dC7cy +WZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0yLSh+aXsPYhjns4AbjGmiKOjqd5w +sPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8RI4Zh3cADI1I5fe6wk1ETN+30cDw +dGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx7wIDAQABAoIBAGxj5pZpTZ4msnEL +ASQnY9oBS4ZXr8UmaamgU/mADDOR2JR4T0ngWeNvtSPG/GV70TgO9B7U8oJoFoyh +05jCEXjmO5vfSNDs7rv6oUMONKczvybABKGMRgD5F8hhGyXCvGBLwV7u3OvXbw0b +PlNcIbTsJpNkNam0CvDyyc3iZOq+HjIqituREV7lDw0rFeAR2YfEWn4VjZsQRZUZ +XkpQJ5silrXgGemIEGqVA4YyM7i2HmTiLozfVYaVckMc02VFgOaoK9Z/wGlBxtS5 +evc/IGErSA4dc7uXBEeVjhtZoBkof2JV9BNt4hl4KN9wX3tkEX5Aq1K2lirSmg2r +k+UEtwkCgYEA/5uYg25OR+jCFY/7uNS8e32Re1lgDeO+TeT1m+hcF1gCb2GBLifL +yprnuytaz1/mPqawfwbilaxntLBoa5cmNKB3zDsgv4sM451yGZ0oxU0dXpDVHblu +3nhxcaOXtb8jiSsr2MqgMbFlu7m8OupIliS+s8Pq72s6HUQQRKbJ+9MCgYEA4hQl +1W/7nDI2SR4Q3UapQnaUjmDVxX5OD+E4RpKuRF6xF7Ao2CLZusMVo8WN8YiSQP2c +RnzQNKgAVy/1zlhaaQDTs2TmSy9iStbuNZ8P+Gh6kmQXuHxwPyURSmwdpgZdL3+D +8tt6pQNQ0vsLjA9VwHmzIT+rsxPmTxKNvBdNK/UCgYByP6zqyioJMDtYAfRkiAn7 +NIQLW0Z4ztvn2zgAyNoowPjNqgpgg/8t/xEm8tjzKg0y4bSwAnbSqa3s8JCrznKQ +QU1qpt8bXl6TenNeiYWIstA2zYvEbnbkz3b9cT7FSLrse7RsgR0bOQyc3QcKWl+5 +ZJEsrpxbCVV/cUXIObi8awKBgQDOI8rfk+0bXhlrkBOWf/CjnpYUQK2LF4C8MALt +Lp/hzWmyjLihYx2eknUv0Fl966ZXxidxiisaaDlvRlbeIGfHqK5fu9fUpE7+qH2p +vPCF81YYF1YdrLF4kiby8iQSl2juf1nj3kY1IhHXXnsH6Y+qIg24emLntXRhkyxT +XffK5QKBgGbzEvVgDkerw1SiefAaZnLumJJXBlKjJ00Sq8YLeViyFC/sr4EfG/cV +7VYRhBw3e7RcYSBAA7uv8i3iIeCFjFooIZUARqXk4+yW753tY5nSJTWfkR7Bp5Pa +9jKloxckbZKMjH23a+ABOxomY3l93KOBvjLvMYqccuREOwaT12cn +-----END RSA PRIVATE KEY----- +]]; + + alt_rsa_public_pem = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4bt6kor2TomqRXfjCFe6 +T42ibatloyHntZCUdlDDAkUh4oJ/4jDCXAUMYqmEsZKCPXxUGQgrmSmNnJPEDMTq +3XLDsjhyN4stxEi0UVAiqqBkcEnkqbQIJSc9v5gpQF8IuJFWRvSNic0uClFL5W9R +2s5AHcOhdFYKeDuitqHT5r+dC7cyWZs5YleKaESxmK6i6wMVhL9adAilTuETyMH0 +yLSh+aXsPYhjns4AbjGmiKOjqd5wsPwllEg6rGcIUi/o79z9HN8yLMXq3XNFCCA8 +RI4Zh3cADI1I5fe6wk1ETN+30cDwdGQ+uQbaQrzqmKVRNjZcorMwBjsOX5AMQBFx +7wIDAQAB +-----END PUBLIC KEY----- +]]; +}; + +return test_keys; diff -r 54a936345aaa -r 5d9ec2e55d74 spec/net_resolvers_service_spec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/net_resolvers_service_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,241 @@ +local set = require "util.set"; + +insulate("net.resolvers.service", function () + local adns = { + resolver = function () + return { + lookup = function (_, cb, qname, qtype, qclass) + if qname == "_xmpp-server._tcp.example.com" + and (qtype or "SRV") == "SRV" + and (qclass or "IN") == "IN" then + cb({ + { -- 60+35+60 + srv = { target = "xmpp0-a.example.com", port = 5228, priority = 0, weight = 60 }; + }; + { + srv = { target = "xmpp0-b.example.com", port = 5216, priority = 0, weight = 35 }; + }; + { + srv = { target = "xmpp0-c.example.com", port = 5200, priority = 0, weight = 0 }; + }; + { + srv = { target = "xmpp0-d.example.com", port = 5256, priority = 0, weight = 120 }; + }; + + { + srv = { target = "xmpp1-a.example.com", port = 5273, priority = 1, weight = 30 }; + }; + { + srv = { target = "xmpp1-b.example.com", port = 5274, priority = 1, weight = 30 }; + }; + + { + srv = { target = "xmpp2.example.com", port = 5275, priority = 2, weight = 0 }; + }; + }); + elseif qname == "_xmpp-server._tcp.single.example.com" + and (qtype or "SRV") == "SRV" + and (qclass or "IN") == "IN" then + cb({ + { + srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 }; + }; + }); + elseif qname == "_xmpp-server._tcp.half.example.com" + and (qtype or "SRV") == "SRV" + and (qclass or "IN") == "IN" then + cb({ + { + srv = { target = "xmpp0-a.example.com", port = 5269, priority = 0, weight = 0 }; + }; + { + srv = { target = "xmpp0-b.example.com", port = 5270, priority = 0, weight = 1 }; + }; + }); + elseif qtype == "A" then + local l = qname:match("%-(%a)%.example.com$") or "1"; + local d = ("%d"):format(l:byte()) + cb({ + { + a = "127.0.0."..d; + }; + }); + elseif qtype == "AAAA" then + local l = qname:match("%-(%a)%.example.com$") or "1"; + local d = ("%04d"):format(l:byte()) + cb({ + { + aaaa = "fdeb:9619:649e:c7d9::"..d; + }; + }); + else + cb(nil); + end + end; + }; + end; + }; + package.loaded["net.adns"] = mock(adns); + local resolver = require "net.resolvers.service"; + math.randomseed(os.time()); + it("works for 99% of deployments", function () + -- Most deployments only have a single SRV record, let's make + -- sure that works okay + + local expected_targets = set.new({ + -- xmpp0-a + "tcp4 127.0.0.97 5269"; + "tcp6 fdeb:9619:649e:c7d9::0097 5269"; + }); + local received_targets = set.new({}); + + local r = resolver.new("single.example.com", "xmpp-server"); + local done = false; + local function handle_target(...) + if ... == nil then + done = true; + -- No more targets + return; + end + received_targets:add(table.concat({ ... }, " ", 1, 3)); + end + r:next(handle_target); + while not done do + r:next(handle_target); + end + + -- We should have received all expected targets, and no unexpected + -- ones: + assert.truthy(set.xor(received_targets, expected_targets):empty()); + end); + + it("supports A/AAAA fallback", function () + -- Many deployments don't have any SRV records, so we should + -- fall back to A/AAAA records instead when that is the case + + local expected_targets = set.new({ + -- xmpp0-a + "tcp4 127.0.0.97 5269"; + "tcp6 fdeb:9619:649e:c7d9::0097 5269"; + }); + local received_targets = set.new({}); + + local r = resolver.new("xmpp0-a.example.com", "xmpp-server", "tcp", { default_port = 5269 }); + local done = false; + local function handle_target(...) + if ... == nil then + done = true; + -- No more targets + return; + end + received_targets:add(table.concat({ ... }, " ", 1, 3)); + end + r:next(handle_target); + while not done do + r:next(handle_target); + end + + -- We should have received all expected targets, and no unexpected + -- ones: + assert.truthy(set.xor(received_targets, expected_targets):empty()); + end); + + + it("works", function () + local expected_targets = set.new({ + -- xmpp0-a + "tcp4 127.0.0.97 5228"; + "tcp6 fdeb:9619:649e:c7d9::0097 5228"; + "tcp4 127.0.0.97 5273"; + "tcp6 fdeb:9619:649e:c7d9::0097 5273"; + + -- xmpp0-b + "tcp4 127.0.0.98 5274"; + "tcp6 fdeb:9619:649e:c7d9::0098 5274"; + "tcp4 127.0.0.98 5216"; + "tcp6 fdeb:9619:649e:c7d9::0098 5216"; + + -- xmpp0-c + "tcp4 127.0.0.99 5200"; + "tcp6 fdeb:9619:649e:c7d9::0099 5200"; + + -- xmpp0-d + "tcp4 127.0.0.100 5256"; + "tcp6 fdeb:9619:649e:c7d9::0100 5256"; + + -- xmpp2 + "tcp4 127.0.0.49 5275"; + "tcp6 fdeb:9619:649e:c7d9::0049 5275"; + + }); + local received_targets = set.new({}); + + local r = resolver.new("example.com", "xmpp-server"); + local done = false; + local function handle_target(...) + if ... == nil then + done = true; + -- No more targets + return; + end + received_targets:add(table.concat({ ... }, " ", 1, 3)); + end + r:next(handle_target); + while not done do + r:next(handle_target); + end + + -- We should have received all expected targets, and no unexpected + -- ones: + assert.truthy(set.xor(received_targets, expected_targets):empty()); + end); + + it("balances across weights correctly #slow", function () + -- This mimics many repeated connections to 'example.com' (mock + -- records defined above), and records the port number of the + -- first target. Therefore it (should) only return priority + -- 0 records, and the input data is constructed such that the + -- last two digits of the port number represent the percentage + -- of times that record should (on average) be picked first. + + -- To prevent random test failures, we test across a handful + -- of fixed (randomly selected) seeds. + for _, seed in ipairs({ 8401877, 3943829, 7830992 }) do + math.randomseed(seed); + + local results = {}; + local function run() + local run_results = {}; + local r = resolver.new("example.com", "xmpp-server"); + local function record_target(...) + if ... == nil then + -- No more targets + return; + end + run_results = { ... }; + end + r:next(record_target); + return run_results[3]; + end + + for _ = 1, 1000 do + local port = run(); + results[port] = (results[port] or 0) + 1; + end + + local ports = {}; + for port in pairs(results) do + table.insert(ports, port); + end + table.sort(ports); + for _, port in ipairs(ports) do + --print("PORT", port, tostring((results[port]/1000) * 100).."% hits (expected "..tostring(port-5200).."%)"); + local hit_pct = (results[port]/1000) * 100; + local expected_pct = port - 5200; + --print(hit_pct, expected_pct, math.abs(hit_pct - expected_pct)); + assert.is_true(math.abs(hit_pct - expected_pct) < 5); + end + --print("---"); + end + end); +end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/extdisco.scs --- a/spec/scansion/extdisco.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/extdisco.scs Wed Mar 27 15:39:03 2024 +0000 @@ -17,8 +17,8 @@ - - + + @@ -47,8 +47,8 @@ Romeo receives: - - + + diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/lastactivity.scs --- a/spec/scansion/lastactivity.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/lastactivity.scs Wed Mar 27 15:39:03 2024 +0000 @@ -37,7 +37,7 @@ Romeo receives: - Goodbye + Goodbye Romeo disconnects diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/mam_extended.scs --- a/spec/scansion/mam_extended.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/mam_extended.scs Wed Mar 27 15:39:03 2024 +0000 @@ -45,8 +45,8 @@ Romeo receives: - - + + @@ -57,9 +57,9 @@ Romeo receives: - + - + Hello @@ -69,9 +69,9 @@ Romeo receives: - + - + U there? @@ -98,7 +98,7 @@ - + U there? @@ -110,7 +110,7 @@ - + Hello diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/muc_outcast_reason.scs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/scansion/muc_outcast_reason.scs Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,72 @@ +# Save ban reason + +[Client] Romeo + password: password + jid: user@localhost + +----- + +Romeo connects + +Romeo sends: + + + + +Romeo receives: + + + + + + + + +Romeo receives: + + + + +Romeo sends: + + + + Hey calm down + + + + +Romeo receives: + + + + + Hey calm down + + + + +Romeo receives: + + +Romeo sends: + + + + + + +Romeo receives: + + + + Hey calm down + + + + +Romeo disconnects + +Romeo sends: + + diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/muc_subject_issue_667.scs --- a/spec/scansion/muc_subject_issue_667.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/muc_subject_issue_667.scs Wed Mar 27 15:39:03 2024 +0000 @@ -42,6 +42,21 @@ Hello everyone +# this should be treated as a normal message +Romeo sends: + + New thread + 498acea5-5894-473f-b4c6-c77319d11c75 + + + +Romeo receives: + + New thread + 498acea5-5894-473f-b4c6-c77319d11c75 + + + # Resync Romeo sends: @@ -63,6 +78,13 @@ Hello everyone +Romeo receives: + + New thread + 498acea5-5894-473f-b4c6-c77319d11c75 + + + # the still empty subject Romeo receives: @@ -116,6 +138,13 @@ Romeo receives: + New thread + 498acea5-5894-473f-b4c6-c77319d11c75 + + + +Romeo receives: + Lorem ipsum dolor sit amet diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/pep_itemreply.scs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/scansion/pep_itemreply.scs Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,205 @@ +# PEP itemreply (publisher) configuration +# This tests that itemreply == "publisher" will add the 'publisher' attribute +# to notifications. Since this is not the default behaviour, the normal +# publish and subscribe test cases cover testing that it is not included +# otherwise. + +[Client] Romeo + jid: pep-test-df6zdvkv@localhost + password: password + +[Client] Juliet + jid: pep-test-5k90xvps@localhost + password: password + +----- + +Romeo connects + +Romeo sends: + + + + +Romeo receives: + + + + +Romeo receives: + + + + +Romeo sends: + + + + +Romeo receives: + + + + +Juliet connects + +Juliet sends: + + + + +Juliet receives: + + + + +Juliet receives: + + + + +Juliet sends: + + + + +Juliet receives: + + + + +Romeo sends: + + +Romeo sends: + + +Romeo receives: + + +Romeo receives: + + +Juliet receives: + + +Juliet sends: + + +Juliet sends: + + +Juliet receives: + + +Juliet receives: + + +Romeo receives: + + +Romeo sends: + + +Romeo sends: + + +Romeo receives: + + +Juliet receives: + + +Juliet receives: + + +Juliet receives: + + +Juliet sends: + + +Juliet receives: + + +Juliet receives: + + +Romeo receives: + + +Romeo receives: + + +Romeo receives: + + +Juliet sends: + + +Romeo sends: + + +Romeo sends: + + +Romeo sends: + + +Romeo receives: + + +Romeo receives: + + +Romeo receives: + + +Juliet receives: + + +Romeo sends: + + +Romeo receives: + + +Romeo receives: + + +Romeo receives: + + +Romeo sends: + + +Romeo sends: + + +Romeo sends: + + +Juliet receives: + + +Juliet sends: + + +Juliet sends: + Beautiful CedarsThe SpinnersNot Quite Folk4http://jabber.org/protocol/pubsub#publish-optionstruepublisher + +Juliet receives: + + +Romeo receives: + Beautiful CedarsThe SpinnersNot Quite Folk4 + +Romeo sends: + + +Romeo receives: + Beautiful CedarsThe SpinnersNot Quite Folk4 + +Juliet disconnects + +Romeo disconnects diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/pep_nickname.scs --- a/spec/scansion/pep_nickname.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/pep_nickname.scs Wed Mar 27 15:39:03 2024 +0000 @@ -58,7 +58,7 @@ - + diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/pep_publish_subscribe.scs --- a/spec/scansion/pep_publish_subscribe.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/pep_publish_subscribe.scs Wed Mar 27 15:39:03 2024 +0000 @@ -182,7 +182,7 @@ Juliet sends: - Beautiful CedarsThe SpinnersNot Quite Folk4 + Beautiful CedarsThe SpinnersNot Quite Folk4 Juliet receives: @@ -197,13 +197,13 @@ Romeo receives: - Beautiful CedarsThe SpinnersNot Quite Folk4 + Beautiful CedarsThe SpinnersNot Quite Folk4 Romeo sends: Romeo receives: - Beautiful CedarsThe SpinnersNot Quite Folk4 + Beautiful CedarsThe SpinnersNot Quite Folk4 Juliet disconnects diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/prosody.cfg.lua --- a/spec/scansion/prosody.cfg.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/prosody.cfg.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,20 +1,9 @@ --luacheck: ignore --- Mock time functions to simplify tests -function _G.os.time() - return 1219439344; -end -package.preload["util.time"] = function () - return { - now = function () return 1219439344.1; end; - monotonic = function () return 0.1; end; - } -end - admins = { "admin@localhost" } network_backend = ENV_PROSODY_NETWORK_BACKEND or "epoll" -network_settings = require"util.json".decode(ENV_PROSODY_NETWORK_SETTINGS or "{}") +network_settings = Lua.require"prosody.util.json".decode(ENV_PROSODY_NETWORK_SETTINGS or "{}") modules_enabled = { -- Generally required @@ -66,6 +55,9 @@ "tombstones"; "user_account_management"; + -- Required for integration testing + "debug_reset"; + -- Useful for testing --"scansion_record"; -- Records things that happen in scansion test case format } diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/pubsub_config.scs --- a/spec/scansion/pubsub_config.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/pubsub_config.scs Wed Mar 27 15:39:03 2024 +0000 @@ -119,6 +119,18 @@ 1 + + + + + none + diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/pubsub_max_items.scs --- a/spec/scansion/pubsub_max_items.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/pubsub_max_items.scs Wed Mar 27 15:39:03 2024 +0000 @@ -114,6 +114,18 @@ 1 + + + + + none + diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/pubsub_multi_items.scs --- a/spec/scansion/pubsub_multi_items.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/pubsub_multi_items.scs Wed Mar 27 15:39:03 2024 +0000 @@ -114,6 +114,18 @@ 1 + + + + + none + diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/pubsub_preconditions.scs --- a/spec/scansion/pubsub_preconditions.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/pubsub_preconditions.scs Wed Mar 27 15:39:03 2024 +0000 @@ -118,6 +118,18 @@ 1 + + + + + none + @@ -199,6 +211,9 @@ 1 + + none + diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/uptime.scs --- a/spec/scansion/uptime.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/uptime.scs Wed Mar 27 15:39:03 2024 +0000 @@ -15,7 +15,7 @@ Romeo receives: - + Romeo disconnects diff -r 54a936345aaa -r 5d9ec2e55d74 spec/scansion/vcard_temp.scs --- a/spec/scansion/vcard_temp.scs Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/scansion/vcard_temp.scs Wed Mar 27 15:39:03 2024 +0000 @@ -51,8 +51,6 @@ -Romeo disconnects - Juliet connects Juliet sends: @@ -77,4 +75,6 @@ Juliet disconnects +Romeo disconnects + # recording ended on 2018-10-20T15:02:14Z diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_argparse_spec.lua --- a/spec/util_argparse_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_argparse_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -50,4 +50,9 @@ assert.equal("-h", where, "returned where"); end); + it("supports array arguments", function () + local opts, err = parse({ "--item"; "foo"; "--item"; "bar" }, { array_params = { item = true } }); + assert.falsy(err); + assert.same({"foo","bar"}, opts.item); + end) end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_bitcompat_spec.lua --- a/spec/util_bitcompat_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_bitcompat_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -24,4 +24,8 @@ it("lshift works", function () assert.equal(0xFF00, bit.lshift(0xFF, 8)); end); + + it("bnot works", function () + assert.equal(0x0000FF00, bit.band(0xFFFFFFFF, bit.bnot(0xFFFF00FF))); + end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_cache_spec.lua --- a/spec/util_cache_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_cache_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -4,6 +4,20 @@ describe("util.cache", function() describe("#new()", function() it("should work", function() + do + local c = cache.new(1); + assert.is_not_nil(c); + + assert.has_error(function () + cache.new(0); + end); + assert.has_error(function () + cache.new(-1); + end); + assert.has_error(function () + cache.new("foo"); + end); + end local c = cache.new(5); @@ -314,7 +328,7 @@ end); - (_VERSION=="Lua 5.1" and pending or it)(":table works", function () + it(":table works", function () local t = cache.new(3):table(); assert.is.table(t); t["a"] = "1"; @@ -336,5 +350,63 @@ assert.spy(i).was_called_with("c", "3"); assert.spy(i).was_called_with("d", "4"); end); + + local function vs(t) + local vs_ = {}; + for v in t:values() do + vs_[#vs_+1] = v; + end + return vs_; + end + + it(":values works", function () + local t = cache.new(3); + t:set("k1", "v1"); + t:set("k2", "v2"); + assert.same({"v2", "v1"}, vs(t)); + t:set("k3", "v3"); + assert.same({"v3", "v2", "v1"}, vs(t)); + t:set("k4", "v4"); + assert.same({"v4", "v3", "v2"}, vs(t)); + end); + + it(":resize works", function () + local c = cache.new(5); + for i = 1, 5 do + c:set(("k%d"):format(i), ("v%d"):format(i)); + end + assert.same({"v5", "v4", "v3", "v2", "v1"}, vs(c)); + assert.has_error(function () + c:resize(-1); + end); + assert.has_error(function () + c:resize(0); + end); + assert.has_error(function () + c:resize("foo"); + end); + c:resize(3); + assert.same({"v5", "v4", "v3"}, vs(c)); + end); + + it("eviction stuff", function () + local c = cache.new(4, function(_k,_v,c) + if c.size < 10 then + c:resize(c.size*2); + end + end) + for i = 1,20 do + c:set(i,i) + end + assert.equal(16, c.size); + assert.is_nil(c:get(1)) + assert.is_nil(c:get(4)) + assert.equal(5, c:get(5)) + assert.equal(20, c:get(20)) + c:resize(4) + assert.equal(20, c:get(20)) + assert.equal(17, c:get(17)) + assert.is_nil(c:get(10)) + end) end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_crypto_spec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/util_crypto_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,184 @@ +local test_keys = require "spec.inputs.test_keys"; + +describe("util.crypto", function () + local crypto = require "util.crypto"; + local random = require "util.random"; + + describe("generate_ed25519_keypair", function () + local keypair = crypto.generate_ed25519_keypair(); + assert.is_not_nil(keypair); + assert.equal("ED25519", keypair:get_type()); + end) + + describe("import_private_pem", function () + it("can import ECDSA keys", function () + local ecdsa_key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + assert.equal("id-ecPublicKey", ecdsa_key:get_type()); + end); + + it("can import EdDSA (Ed25519) keys", function () + local ed25519_key = crypto.import_private_pem(crypto.generate_ed25519_keypair():private_pem()); + assert.equal("ED25519", ed25519_key:get_type()); + end); + + it("can import RSA keys", function () + -- TODO + end); + + it("rejects invalid keys", function () + assert.is_nil(crypto.import_private_pem(test_keys.eddsa_public_pem)); + assert.is_nil(crypto.import_private_pem(test_keys.ecdsa_public_pem)); + assert.is_nil(crypto.import_private_pem("foo")); + assert.is_nil(crypto.import_private_pem("")); + end); + end); + + describe("import_public_pem", function () + it("can import ECDSA public keys", function () + local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem); + assert.equal("id-ecPublicKey", ecdsa_key:get_type()); + end); + + it("can import EdDSA (Ed25519) public keys", function () + local ed25519_key = crypto.import_public_pem(test_keys.eddsa_public_pem); + assert.equal("ED25519", ed25519_key:get_type()); + end); + + it("can import RSA public keys", function () + -- TODO + end); + end); + + describe("PEM export", function () + it("works", function () + local ecdsa_key = crypto.import_public_pem(test_keys.ecdsa_public_pem); + assert.equal("id-ecPublicKey", ecdsa_key:get_type()); + assert.equal(test_keys.ecdsa_public_pem, ecdsa_key:public_pem()); + + assert.has_error(function () + -- Fails because private key is not available + ecdsa_key:private_pem(); + end); + + local ecdsa_private_key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + assert.equal(test_keys.ecdsa_private_pem, ecdsa_private_key:private_pem()); + end); + end); + + describe("sign/verify with", function () + local test_cases = { + ed25519 = { + crypto.ed25519_sign, crypto.ed25519_verify; + key = crypto.import_private_pem(test_keys.eddsa_private_pem); + sig_length = 64; + }; + ecdsa = { + crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify; + key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + }; + }; + for test_name, test in pairs(test_cases) do + local key = test.key; + describe(test_name, function () + it("works", function () + local sign, verify = test[1], test[2]; + local sig = assert(sign(key, "Hello world")); + assert.is_string(sig); + if test.sig_length then + assert.equal(test.sig_length, #sig); + end + + do + local ok = verify(key, "Hello world", sig); + assert.is_truthy(ok); + end + do -- Incorrect signature + local ok = verify(key, "Hello world", sig:sub(1, -2)..string.char((sig:byte(-1)+1)%255)); + assert.is_falsy(ok); + end + do -- Incorrect message + local ok = verify(key, "Hello earth", sig); + assert.is_falsy(ok); + end + do -- Incorrect message (embedded NUL) + local ok = verify(key, "Hello world\0foo", sig); + assert.is_falsy(ok); + end + end); + end); + end + end); + + describe("ECDSA signatures", function () + local hex = require "util.hex"; + local sig = hex.decode((([[ + 304402203e936e7b0bc62887e0e9d675afd08531a930384cfcf301 + f25d13053a2ebf141d02205a5a7c7b7ac5878d004cb79b17b39346 + 6b0cd1043718ffc31c153b971d213a8e + ]]):gsub("%s+", ""))); + it("can be parsed", function () + local r, s = crypto.parse_ecdsa_signature(sig, 32); + assert.is_string(r); + assert.is_string(s); + assert.equal(32, #r); + assert.equal(32, #s); + end); + it("fails to parse invalid signatures", function () + local invalid_sigs = { + ""; + "\000"; + string.rep("\000", 64); + string.rep("\000", 72); + string.rep("\000", 256); + string.rep("\255", 72); + string.rep("\255", 3); + }; + for _, invalid_sig in ipairs(invalid_sigs) do + local r, s = crypto.parse_ecdsa_signature(invalid_sig, 32); + assert.is_nil(r); + assert.is_nil(s); + end + end); + it("can be built", function () + local r, s = crypto.parse_ecdsa_signature(sig, 32); + local rebuilt_sig = crypto.build_ecdsa_signature(r, s); + assert.equal(sig, rebuilt_sig); + end); + end); + + describe("AES-GCM encryption", function () + it("works", function () + local message = "foo\0bar"; + local key_128_bit = random.bytes(16); + local key_256_bit = random.bytes(32); + local test_cases = { + { crypto.aes_128_gcm_encrypt, crypto.aes_128_gcm_decrypt, key = key_128_bit }; + { crypto.aes_256_gcm_encrypt, crypto.aes_256_gcm_decrypt, key = key_256_bit }; + }; + for _, params in pairs(test_cases) do + local iv = params.iv or random.bytes(12); + local encrypted = params[1](params.key, iv, message); + assert.not_equal(message, encrypted); + local decrypted = params[2](params.key, iv, encrypted); + assert.equal(message, decrypted); + end + end); + end); + + describe("AES-CTR encryption", function () + it("works", function () + local message = "foo\0bar hello world"; + local key_256_bit = random.bytes(32); + local test_cases = { + { crypto.aes_256_ctr_decrypt, crypto.aes_256_ctr_decrypt, key = key_256_bit }; + }; + for _, params in pairs(test_cases) do + local iv = params.iv or random.bytes(16); + local encrypted = params[1](params.key, iv, message); + assert.not_equal(message, encrypted); + local decrypted = params[2](params.key, iv, encrypted); + assert.equal(message, decrypted); + end + end); + end); +end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_dataforms_spec.lua --- a/spec/util_dataforms_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_dataforms_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -130,7 +130,7 @@ assert.truthy(st.is_stanza(xform)); assert.equal("x", xform.name); assert.equal("jabber:x:data", xform.attr.xmlns); - assert.equal("FORM_TYPE", xform:find("field@var")); + assert.equal("FORM_TYPE", xform:get_child_attr("field", nil, "var")); assert.equal("xmpp:prosody.im/spec/util.dataforms#1", xform:find("field/value#")); local allowed_direct_children = { title = true, diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_datamapper_spec.lua --- a/spec/util_datamapper_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_datamapper_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -15,22 +15,22 @@ setup(function() -- a convenience function for simple attributes, there's a few of them - local function attr() return {["$ref"]="#/$defs/attr"} end + local attr = {["$ref"]="#/$defs/attr"}; s = { ["$defs"] = { attr = { type = "string"; xml = { attribute = true } } }; type = "object"; xml = {name = "message"; namespace = "jabber:client"}; properties = { - to = attr(); - from = attr(); - type = attr(); - id = attr(); + to = attr; + from = attr; + type = attr; + id = attr; body = true; -- should be assumed to be a string lang = {type = "string"; xml = {attribute = true; prefix = "xml"}}; delay = { type = "object"; xml = {namespace = "urn:xmpp:delay"; name = "delay"}; - properties = {stamp = attr(); from = attr(); reason = {type = "string"; xml = {text = true}}}; + properties = {stamp = attr; from = attr; reason = {type = "string"; xml = {text = true}}}; }; state = { type = "string"; @@ -66,8 +66,8 @@ xml = {name = "stanza-id"; namespace = "urn:xmpp:sid:0"}; type = "object"; properties = { - id = attr(); - by = attr(); + id = attr; + by = attr; }; }; }; @@ -120,10 +120,10 @@ namespace = "jabber:client" }; properties = { - to = attr(); - from = attr(); - type = attr(); - id = attr(); + to = attr; + from = attr; + type = attr; + id = attr; disco = { type = "object"; xml = { diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_datetime_spec.lua --- a/spec/util_datetime_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_datetime_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -16,7 +16,10 @@ assert.truthy(string.find(date(), "^%d%d%d%d%-%d%d%-%d%d$")); end); it("should work", function () - assert.equals(date(1136239445), "2006-01-02"); + assert.equals("2006-01-02", date(1136239445)); + end); + it("should ignore fractional parts", function () + assert.equals("2006-01-02", date(1136239445.5)); end); end); describe("#time", function () @@ -32,8 +35,14 @@ assert.truthy(string.find(time(), "^%d%d:%d%d:%d%d")); end); it("should work", function () - assert.equals(time(1136239445), "22:04:05"); + assert.equals("22:04:05", time(1136239445)); end); + it("should handle precision", function () + assert.equal("14:46:31.158200", time(1660488391.1582)) + assert.equal("14:46:32.158200", time(1660488392.1582)) + assert.equal("14:46:33.158200", time(1660488393.1582)) + assert.equal("14:46:33.999900", time(1660488393.9999)) + end) end); describe("#datetime", function () local datetime = util_datetime.datetime; @@ -48,14 +57,23 @@ assert.truthy(string.find(datetime(), "^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d")); end); it("should work", function () - assert.equals(datetime(1136239445), "2006-01-02T22:04:05Z"); + assert.equals("2006-01-02T22:04:05Z", datetime(1136239445)); end); + it("should handle precision", function () + assert.equal("2022-08-14T14:46:31.158200Z", datetime(1660488391.1582)) + assert.equal("2022-08-14T14:46:32.158200Z", datetime(1660488392.1582)) + assert.equal("2022-08-14T14:46:33.158200Z", datetime(1660488393.1582)) + assert.equal("2022-08-14T14:46:33.999900Z", datetime(1660488393.9999)) + end) end); describe("#legacy", function () local legacy = util_datetime.legacy; it("should exist", function () assert.is_function(legacy); end); + it("should not add precision", function () + assert.equal("20220814T14:46:31", legacy(1660488391.1582)); + end); end); describe("#parse", function () local parse = util_datetime.parse; @@ -64,13 +82,23 @@ end); it("should work", function () -- Timestamp used by Go - assert.equals(parse("2017-11-19T17:58:13Z"), 1511114293); - assert.equals(parse("2017-11-19T18:58:50+0100"), 1511114330); - assert.equals(parse("2006-01-02T15:04:05-0700"), 1136239445); + assert.equals(1511114293, parse("2017-11-19T17:58:13Z")); + assert.equals(1511114330, parse("2017-11-19T18:58:50+0100")); + assert.equals(1136239445, parse("2006-01-02T15:04:05-0700")); + assert.equals(1136239445, parse("2006-01-02T15:04:05-07")); end); it("should handle timezones", function () -- https://xmpp.org/extensions/xep-0082.html#example-2 and 3 assert.equals(parse("1969-07-21T02:56:15Z"), parse("1969-07-20T21:56:15-05:00")); end); + it("should handle precision", function () + -- floating point comparison is not an exact science + assert.truthy(math.abs(1660488392.1582 - parse("2022-08-14T14:46:32.158200Z")) < 0.001) + end) + it("should return nil when given invalid inputs", function () + assert.is_nil(parse(nil)); + assert.is_nil(parse("hello world")); + assert.is_nil(parse("2017-11-19T18:58:50$0100")); + end); end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_dbuffer_spec.lua --- a/spec/util_dbuffer_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_dbuffer_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,6 +6,8 @@ end); it("can be created", function () assert.truthy(dbuffer.new()); + assert.truthy(dbuffer.new(1)); + assert.truthy(dbuffer.new(1024)); end); it("won't create an empty buffer", function () assert.falsy(dbuffer.new(0)); @@ -15,10 +17,21 @@ end); end); describe(":write", function () - local b = dbuffer.new(); + local b = dbuffer.new(10, 3); it("works", function () assert.truthy(b:write("hi")); end); + it("fails when the buffer is full", function () + local ret = b:write(" there world, this is a long piece of data"); + assert.is_falsy(ret); + end); + it("works when max_chunks is reached", function () + -- Chunks are an optimization, dbuffer should collapse chunks when needed + for _ = 1, 8 do + assert.truthy(b:write("!")); + end + assert.falsy(b:write("!")); -- Length reached + end); end); describe(":read", function () @@ -34,6 +47,14 @@ assert.equal(" ", b:read()); assert.equal("world", b:read()); end); + it("fails when there is not enough data in the buffer", function () + local b = dbuffer.new(12); + b:write("hello"); + b:write(" "); + b:write("world"); + assert.is_falsy(b:read(12)); + assert.is_falsy(b:read(13)); + end); end); describe(":read_until", function () @@ -68,9 +89,46 @@ assert.equal(5, b:len()); assert.equal("world", b:read(5)); end); + it("works across chunks", function () + assert.truthy(b:write("hello")); + assert.truthy(b:write(" ")); + assert.truthy(b:write("world")); + assert.truthy(b:discard(3)); + assert.equal(8, b:length()); + assert.truthy(b:discard(3)); + assert.equal(5, b:length()); + assert.equal("world", b:read(5)); + end); + it("can discard the entire buffer", function () + assert.equal(b:len(), 0); + assert.truthy(b:write("hello world")); + assert.truthy(b:discard(11)); + assert.equal(0, b:len()); + assert.truthy(b:write("hello world")); + assert.truthy(b:discard(12)); + assert.equal(0, b:len()); + assert.truthy(b:write("hello world")); + assert.truthy(b:discard(128)); + assert.equal(0, b:len()); + end); + it("works on an empty buffer", function () + assert.truthy(dbuffer.new():discard()); + assert.truthy(dbuffer.new():discard(0)); + assert.truthy(dbuffer.new():discard(1)); + end); end); describe(":collapse()", function () + it("works", function () + local b = dbuffer.new(); + b:write("hello"); + b:write(" "); + b:write("world"); + b:collapse(6); + local ret, bytes = b:read_chunk(); + assert.equal("hello ", ret); + assert.equal(6, bytes); + end); it("works on an empty buffer", function () local b = dbuffer.new(); b:collapse(); @@ -115,6 +173,11 @@ end end end); + + it("works on an empty buffer", function () + local b = dbuffer.new(); + assert.equal("", b:sub(1, 12)); + end); end); describe(":byte", function () @@ -122,7 +185,11 @@ local s = "hello world" local function test_byte(b, x, y) local string_result, buffer_result = {s:byte(x, y)}, {b:byte(x, y)}; - assert.same(string_result, buffer_result, ("buffer:byte(%d, %s) does not match string:byte()"):format(x, y and ("%d"):format(y) or "nil")); + assert.same( + string_result, + buffer_result, + ("buffer:byte(%s, %s) does not match string:byte()"):format(x and ("%d"):format(x) or "nil", y and ("%d"):format(y) or "nil") + ); end it("is equivalent to string:byte", function () @@ -132,6 +199,7 @@ test_byte(b, 3); test_byte(b, -1); test_byte(b, -3); + test_byte(b, nil, 5); for i = -13, 13 do for j = -13, 13 do test_byte(b, i, j); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_error_spec.lua --- a/spec/util_error_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_error_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -56,6 +56,9 @@ assert.equal(e, err.context.stanza); assert.equal("error.example", err.context.by); assert.not_nil(err.extra.tag); + assert.not_has_error(function () + errors.from_stanza(st.message()) + end); end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_format_spec.lua --- a/spec/util_format_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_format_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -13,7 +13,7 @@ assert.equal("true", format("%s", true)); assert.equal("[true]", format("%d", true)); assert.equal("% [true]", format("%%", true)); - assert.equal("{ }", format("%q", { })); + assert.equal("{}", format("%q", {})); assert.equal("[1.5]", format("%d", 1.5)); assert.equal("[7.3786976294838e+19]", format("%d", 73786976294838206464)); end); @@ -333,29 +333,27 @@ end); end); - if _VERSION > "Lua 5.1" then -- COMPAT no %a or %A in Lua 5.1 - describe("to %a", function () - it("works", function () - assert.equal("0x1.84p+6", format("%a", 97)) - assert.equal("-0x1.81c8p+13", format("%a", -12345)) - assert.equal("0x1.8p+0", format("%a", 1.5)) - assert.equal("0x1p+66", format("%a", 73786976294838206464)) - assert.equal("inf", format("%a", math.huge)) - assert.equal("0x1.fffffffcp+30", format("%a", 2147483647)) - end); + describe("to %a", function () + it("works", function () + assert.equal("0x1.84p+6", format("%a", 97)) + assert.equal("-0x1.81c8p+13", format("%a", -12345)) + assert.equal("0x1.8p+0", format("%a", 1.5)) + assert.equal("0x1p+66", format("%a", 73786976294838206464)) + assert.equal("inf", format("%a", math.huge)) + assert.equal("0x1.fffffffcp+30", format("%a", 2147483647)) end); + end); - describe("to %A", function () - it("works", function () - assert.equal("0X1.84P+6", format("%A", 97)) - assert.equal("-0X1.81C8P+13", format("%A", -12345)) - assert.equal("0X1.8P+0", format("%A", 1.5)) - assert.equal("0X1P+66", format("%A", 73786976294838206464)) - assert.equal("INF", format("%A", math.huge)) - assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647)) - end); + describe("to %A", function () + it("works", function () + assert.equal("0X1.84P+6", format("%A", 97)) + assert.equal("-0X1.81C8P+13", format("%A", -12345)) + assert.equal("0X1.8P+0", format("%A", 1.5)) + assert.equal("0X1P+66", format("%A", 73786976294838206464)) + assert.equal("INF", format("%A", math.huge)) + assert.equal("0X1.FFFFFFFCP+30", format("%A", 2147483647)) end); - end + end); describe("to %e", function () it("works", function () @@ -670,7 +668,7 @@ describe("to %q", function () it("works", function () - assert.matches('{__type="function",__error="fail"}', format("%q", function() end)) + assert.matches('%[%[function: 0[xX]%x+]]', format("%q", function() end)) end); end); @@ -769,7 +767,7 @@ describe("to %q", function () it("works", function () - assert.matches('{__type="thread",__error="fail"}', format("%q", coroutine.create(function() end))) + assert.matches('_%[%[thread: 0[xX]%x+]]', format("%q", coroutine.create(function() end))) end); end); @@ -882,8 +880,8 @@ describe("to %q", function () it("works", function () - assert.matches("{ }", format("%q", { })) - assert.equal("{ }", format("%q", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) + assert.matches("{}", format("%q", { })) + assert.equal("{}", format("%q", setmetatable({},{__tostring=function ()return "foo \1\2\3 bar"end}))) end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_fsm_spec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/util_fsm_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,250 @@ +describe("util.fsm", function () + local new_fsm = require "util.fsm".new; + + do + local fsm = new_fsm({ + transitions = { + { name = "melt", from = "solid", to = "liquid" }; + { name = "freeze", from = "liquid", to = "solid" }; + }; + }); + + it("works", function () + local water = fsm:init("liquid"); + water:freeze(); + assert.equal("solid", water.state); + water:melt(); + assert.equal("liquid", water.state); + end); + + it("does not allow invalid transitions", function () + local water = fsm:init("liquid"); + assert.has_errors(function () + water:melt(); + end, "Invalid state transition: liquid cannot melt"); + + water:freeze(); + assert.equal("solid", water.state); + + water:melt(); + assert.equal("liquid", water.state); + + assert.has_errors(function () + water:melt(); + end, "Invalid state transition: liquid cannot melt"); + end); + end + + it("notifies observers", function () + local n = 0; + local has_become_solid = spy.new(function (transition) + assert.is_table(transition); + assert.equal("solid", transition.to); + assert.is_not_nil(transition.instance); + n = n + 1; + if n == 1 then + assert.is_nil(transition.from); + assert.is_nil(transition.from_attr); + elseif n == 2 then + assert.equal("liquid", transition.from); + assert.is_nil(transition.from_attr); + assert.equal("freeze", transition.name); + end + end); + local is_melting = spy.new(function (transition) + assert.is_table(transition); + assert.equal("melt", transition.name); + assert.is_not_nil(transition.instance); + end); + local fsm = new_fsm({ + transitions = { + { name = "melt", from = "solid", to = "liquid" }; + { name = "freeze", from = "liquid", to = "solid" }; + }; + state_handlers = { + solid = has_become_solid; + }; + + transition_handlers = { + melt = is_melting; + }; + }); + + local water = fsm:init("liquid"); + assert.spy(has_become_solid).was_not_called(); + + local ice = fsm:init("solid"); --luacheck: ignore 211/ice + assert.spy(has_become_solid).was_called(1); + + water:freeze(); + + assert.spy(is_melting).was_not_called(); + water:melt(); + assert.spy(is_melting).was_called(1); + end); + + local function test_machine(fsm_spec, expected_transitions, test_func) + fsm_spec.handlers = fsm_spec.handlers or {}; + fsm_spec.handlers.transitioned = function (transition) + local expected_transition = table.remove(expected_transitions, 1); + assert.same(expected_transition, { + name = transition.name; + to = transition.to; + to_attr = transition.to_attr; + from = transition.from; + from_attr = transition.from_attr; + }); + end; + local fsm = new_fsm(fsm_spec); + test_func(fsm); + assert.equal(0, #expected_transitions); + end + + + it("handles transitions with the same name", function () + local expected_transitions = { + { name = nil , from = "none", to = "A" }; + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "step", from = "C", to = "D" }; + }; + + test_machine({ + default_state = "none"; + transitions = { + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "step", from = "C", to = "D" }; + }; + }, expected_transitions, function (fsm) + local i = fsm:init("A"); + i:step(); -- B + i:step(); -- C + i:step(); -- D + assert.has_errors(function () + i:step(); + end, "Invalid state transition: D cannot step"); + end); + end); + + it("handles supports wildcard transitions", function () + local expected_transitions = { + { name = nil , from = "none", to = "A" }; + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "reset", from = "C", to = "A" }; + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "step", from = "C", to = "D" }; + }; + + test_machine({ + default_state = "none"; + transitions = { + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "step", from = "C", to = "D" }; + { name = "reset", from = "*", to = "A" }; + }; + }, expected_transitions, function (fsm) + local i = fsm:init("A"); + i:step(); -- B + i:step(); -- C + i:reset(); -- A + i:step(); -- B + i:step(); -- C + i:step(); -- D + assert.has_errors(function () + i:step(); + end, "Invalid state transition: D cannot step"); + end); + end); + + it("supports specifying multiple from states", function () + local expected_transitions = { + { name = nil , from = "none", to = "A" }; + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "reset", from = "C", to = "A" }; + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "step", from = "C", to = "D" }; + }; + + test_machine({ + default_state = "none"; + transitions = { + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "C" }; + { name = "step", from = "C", to = "D" }; + { name = "reset", from = {"B", "C", "D"}, to = "A" }; + }; + }, expected_transitions, function (fsm) + local i = fsm:init("A"); + i:step(); -- B + i:step(); -- C + i:reset(); -- A + assert.has_errors(function () + i:reset(); + end, "Invalid state transition: A cannot reset"); + i:step(); -- B + i:step(); -- C + i:step(); -- D + assert.has_errors(function () + i:step(); + end, "Invalid state transition: D cannot step"); + end); + end); + + it("handles transitions with the same start/end state", function () + local expected_transitions = { + { name = nil , from = "none", to = "A" }; + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "B" }; + { name = "step", from = "B", to = "B" }; + }; + + test_machine({ + default_state = "none"; + transitions = { + { name = "step", from = "A", to = "B" }; + { name = "step", from = "B", to = "B" }; + }; + }, expected_transitions, function (fsm) + local i = fsm:init("A"); + i:step(); -- B + i:step(); -- B + i:step(); -- B + end); + end); + + it("can identify instances of a specific fsm", function () + local fsm1 = new_fsm({ default_state = "a" }); + local fsm2 = new_fsm({ default_state = "a" }); + + local i1 = fsm1:init(); + local i2 = fsm2:init(); + + assert.truthy(fsm1:is_instance(i1)); + assert.truthy(fsm2:is_instance(i2)); + + assert.falsy(fsm1:is_instance(i2)); + assert.falsy(fsm2:is_instance(i1)); + end); + + it("errors when an invalid initial state is passed", function () + local fsm1 = new_fsm({ + transitions = { + { name = "", from = "A", to = "B" }; + }; + }); + + assert.has_no_errors(function () + fsm1:init("A"); + end); + + assert.has_errors(function () + fsm1:init("C"); + end); + end); +end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_hashes_spec.lua --- a/spec/util_hashes_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_hashes_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -4,6 +4,8 @@ -- Also see spec for util.hmac where HMAC test cases reside +--luacheck: ignore 631 + describe("PBKDF2-HMAC-SHA1", function () it("test vector 1", function () local P = "password" @@ -53,3 +55,56 @@ end); +describe("SHA-3", function () + describe("256", function () + it("works", function () + local expected = "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a" + assert.equal(expected, hashes.sha3_256("", true)); + end); + end); + describe("512", function () + it("works", function () + local expected = "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26" + assert.equal(expected, hashes.sha3_512("", true)); + end); + end); +end); + +describe("HKDF", function () + describe("HMAC-SHA256", function () + describe("RFC 5869", function () + it("test vector A.1", function () + local ikm = hex.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + local salt = hex.decode("000102030405060708090a0b0c"); + local info = hex.decode("f0f1f2f3f4f5f6f7f8f9"); + + local expected = "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"; + + local ret = hashes.hkdf_hmac_sha256(42, ikm, salt, info); + assert.equal(expected, hex.encode(ret)); + end); + + it("test vector A.2", function () + local ikm = hex.decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f"); + local salt = hex.decode("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf"); + local info = hex.decode("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + + local expected = "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87"; + + local ret = hashes.hkdf_hmac_sha256(82, ikm, salt, info); + assert.equal(expected, hex.encode(ret)); + end); + + it("test vector A.3", function () + local ikm = hex.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + local salt = ""; + local info = ""; + + local expected = "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"; + + local ret = hashes.hkdf_hmac_sha256(42, ikm, salt, info); + assert.equal(expected, hex.encode(ret)); + end); + end); + end); +end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_hashring_spec.lua --- a/spec/util_hashring_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_hashring_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,7 @@ local hashring = require "util.hashring"; describe("util.hashring", function () + randomize(false); local sha256 = require "util.hashes".sha256; @@ -82,4 +83,11 @@ end end); + it("should support values associated with nodes", function () + local r = hashring.new(128, sha256); + r:add_node("node1", { a = 1 }); + local node, value = r:get_node("foo"); + assert.is_equal("node1", node); + assert.same({ a = 1 }, value); + end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_http_spec.lua --- a/spec/util_http_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_http_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -41,6 +41,7 @@ end); it("should encode special characters with escaping", function() + -- luacheck: ignore 631 assert.are.equal(http.formencode({ { name = "one two", value = "1"}, { name = "two one&", value = "2" } }), "one+two=1&two+one%26=2", "Form encoded"); end); end); @@ -108,4 +109,25 @@ assert.is_(http.contains_token("fo o", "foo")); end); end); + +do + describe("parse_forwarded", function() + it("works", function() + assert.same({ { ["for"] = "[2001:db8:cafe::17]:4711" } }, http.parse_forwarded('For="[2001:db8:cafe::17]:4711"'), "case insensitive"); + + assert.same({ { ["for"] = "192.0.2.60"; proto = "http"; by = "203.0.113.43" } }, http.parse_forwarded('for=192.0.2.60;proto=http;by=203.0.113.43'), + "separated by semicolon"); + + assert.same({ { ["for"] = "192.0.2.43" }; { ["for"] = "198.51.100.17" } }, http.parse_forwarded('for=192.0.2.43, for=198.51.100.17'), + "Values from multiple proxy servers can be appended using a comma"); + + end) + it("rejects quoted quotes", function () + assert.falsy(http.parse_forwarded('foo="bar\"bar'), "quoted quotes"); + end) + pending("deals with quoted quotes", function () + assert.same({ { foo = 'bar"baz' } }, http.parse_forwarded('foo="bar\"bar'), "quoted quotes"); + end) + end) +end end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_human_io_spec.lua --- a/spec/util_human_io_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_human_io_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -42,6 +42,64 @@ assert.equal("räksmörgås", human_io.ellipsis("räksmörgås", 10)); end); end); + + describe("parse_duration", function () + local function test(expected, duration) + return assert.equal(expected, human_io.parse_duration(duration), ("%q -> %d"):format(duration, expected)); + end + local function should_fail(duration) + assert.is_nil(human_io.parse_duration(duration), "invalid duration should fail: %q"); + end + it("works", function () + test(1, "1s"); + test(60, "1min"); + test(60, "1 min"); + test(60, "1 minute"); + test(120, "2min"); + test(7200, "2h"); + test(7200, "2 hours"); + test(86400, "1d"); + test(604800, "1w"); + test(604800, "1week"); + test(1814400, "3 weeks"); + test(2678400, "1month"); + test(2678400, "1 month"); + test(31536000, "365 days"); + test(31556952, "1 year"); + + should_fail("two weeks"); + should_fail("1m"); + should_fail("1mi"); + should_fail("1mo"); + end); + end); + + describe("parse_duration_lax", function () + local function test(expected, duration) + return assert.equal(expected, human_io.parse_duration_lax(duration), ("%q -> %d"):format(duration, expected)); + end + it("works", function () + test(1, "1s"); + test(60, "1mi"); + test(60, "1min"); + test(60, "1 min"); + test(60, "1 minute"); + test(120, "2min"); + test(7200, "2h"); + test(7200, "2 hours"); + test(86400, "1d"); + test(604800, "1w"); + test(604800, "1week"); + test(1814400, "3 weeks"); + test(2678400, "1m"); + test(2678400, "1mo"); + test(2678400, "1month"); + test(2678400, "1 month"); + test(31536000, "365 days"); + test(31556952, "1 year"); + return assert.is_nil(human_io.parse_duration_lax("two weeks"), "\"2 weeks\" -> nil"); + end); + end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_ip_spec.lua --- a/spec/util_ip_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_ip_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -36,6 +36,8 @@ assert.are.equal(match(_"8.8.8.8", _"8.8.0.0", 16), true); assert.are.equal(match(_"8.8.4.4", _"8.8.0.0", 16), true); + + assert.are.equal(match(_"fe80::1", _"fec0::", 10), false); end); end); @@ -98,6 +100,30 @@ assert_cpl6("abcd::1", "abcd::1", 128); assert_cpl6("abcd::abcd", "abcd::", 112); assert_cpl6("abcd::abcd", "abcd::abcd:abcd", 96); + + assert_cpl6("fe80::1", "fec0::", 9); + end); + end); + + describe("#truncate()", function () + it("should work for IPv4", function () + local ip1 = ip.new_ip("192.168.0.1"); + local ip2 = ip.truncate(ip1, 16); + assert.truthy(ip.is_ip(ip2)); + assert.equal("192.168.0.0", ip2.normal); + assert.equal("192.168.0.1", ip1.normal); -- original unmodified + end); + + it("should work for IPv6", function () + local ip1 = ip.new_ip("2001:db8::ff00:42:8329"); + local ip2 = ip.truncate(ip1, 24); + assert.truthy(ip.is_ip(ip2)); + assert.equal("2001:d00::", ip2.normal); + assert.equal("2001:db8::ff00:42:8329", ip1.normal); -- original unmodified + end); + + it("accepts a string", function () + assert.equal("127.0.0.0", ip.truncate("127.0.0.1", 8).normal); end); end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_iterators_spec.lua --- a/spec/util_iterators_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_iterators_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,6 +10,14 @@ end assert.same(output, expect); end); + it("should work with only a single iterator", function () + local expect = { "a", "b", "c" }; + local output = {}; + for x in iter.join(iter.values({"a", "b", "c"})) do + table.insert(output, x); + end + assert.same(output, expect); + end); end); describe("sorted_pairs", function () diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_jid_spec.lua --- a/spec/util_jid_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_jid_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -48,6 +48,47 @@ end) end); + describe("#prepped_split()", function() + local function test(input_jid, expected_node, expected_server, expected_resource) + local rnode, rserver, rresource = jid.prepped_split(input_jid); + assert.are.equal(expected_node, rnode, "split("..tostring(input_jid)..") failed"); + assert.are.equal(expected_server, rserver, "split("..tostring(input_jid)..") failed"); + assert.are.equal(expected_resource, rresource, "split("..tostring(input_jid)..") failed"); + end + + it("should work", function() + -- Valid JIDs + test("node@server", "node", "server", nil ); + test("node@server/resource", "node", "server", "resource" ); + test("server", nil, "server", nil ); + test("server/resource", nil, "server", "resource" ); + test("server/resource@foo", nil, "server", "resource@foo" ); + test("server/resource@foo/bar", nil, "server", "resource@foo/bar"); + + -- Always invalid JIDs + test(nil, nil, nil, nil); + test("node@/server", nil, nil, nil); + test("@server", nil, nil, nil); + test("@server/resource", nil, nil, nil); + test("@/resource", nil, nil, nil); + test("@server/", nil, nil, nil); + test("server/", nil, nil, nil); + test("/resource", nil, nil, nil); + end); + it("should reject invalid arguments", function () + assert.has_error(function () jid.prepped_split(false) end) + end) + it("should strip empty root label", function () + test("node@server.", "node", "server", nil); + end); + it("should fail for JIDs that fail stringprep", function () + test("node@invalid-\128-server", nil, nil, nil); + test("node@invalid-\194\128-server", nil, nil, nil); + test("@server", nil, nil, nil); + test("node@server/invalid-\000-resource", nil, nil, nil); + end); + end); + describe("#bare()", function() it("should work", function() diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_jsonpointer_spec.lua --- a/spec/util_jsonpointer_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_jsonpointer_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -21,9 +21,11 @@ }]]) end) it("works", function() + assert.is_nil(jp.resolve("string", "/string")) assert.same(example, jp.resolve(example, "")); assert.same({ "bar", "baz" }, jp.resolve(example, "/foo")); assert.same("bar", jp.resolve(example, "/foo/0")); + assert.same(nil, jp.resolve(example, "/foo/-")); assert.same(0, jp.resolve(example, "/")); assert.same(1, jp.resolve(example, "/a~1b")); assert.same(2, jp.resolve(example, "/c%d")); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_jsonschema_spec.lua --- a/spec/util_jsonschema_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_jsonschema_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -2,7 +2,7 @@ local json = require "util.json"; local lfs = require "lfs"; --- https://github.com/json-schema-org/JSON-Schema-Test-Suite.git 2.0.0-550-g88d6948 +-- https://github.com/json-schema-org/JSON-Schema-Test-Suite.git 2.0.0-755-g7950d9e local test_suite_dir = "spec/JSON-Schema-Test-Suite/tests/draft2020-12" if lfs.attributes(test_suite_dir, "mode") ~= "directory" then return end @@ -13,30 +13,25 @@ ["additionalProperties.json:1:0"] = "NYI", ["anchor.json"] = "$anchor NYI", ["const.json:1"] = "deepcompare", - ["const.json:13:2"] = "IEEE 754 equality", ["const.json:2"] = "deepcompare", ["const.json:8"] = "deepcompare", ["const.json:9"] = "deepcompare", ["contains.json:0:5"] = "distinguishing objects from arrays", ["defs.json"] = "need built-in meta-schema", - ["dependentRequired.json"] = "NYI", - ["dependentSchemas.json"] = "NYI", + ["dependentSchemas.json:2:2"] = "NYI", -- minProperties ["dynamicRef.json"] = "NYI", ["enum.json:1:3"] = "deepcompare", ["id.json"] = "NYI", - ["maxContains.json"] = "NYI", - ["maxLength.json:0:4"] = "UTF-16", ["maxProperties.json"] = "NYI", - ["minContains.json"] = "NYI", - ["minLength.json:0:4"] = "UTF-16", ["minProperties.json"] = "NYI", ["multipleOf.json:1"] = "multiples of IEEE 754 fractions", ["multipleOf.json:2"] = "multiples of IEEE 754 fractions", + ["multipleOf.json:4"] = "multiples of IEEE 754 fractions", ["pattern.json"] = "NYI", ["patternProperties.json"] = "NYI", ["properties.json:1:2"] = "NYI", ["properties.json:1:3"] = "NYI", - ["ref.json:0:3"] = "NYI additionalProperties", + ["ref.json:0:3"] = "util.jsonpointer recursive issue?", ["ref.json:11"] = "NYI", ["ref.json:12:1"] = "FIXME", ["ref.json:13"] = "NYI", @@ -54,8 +49,14 @@ ["ref.json:6:1"] = "NYI", ["ref.json:20"] = "NYI", ["ref.json:25"] = "NYI", + ["ref.json:29"] = "NYI", + ["ref.json:30"] = "NYI", + ["ref.json:31"] = "NYI", + ["ref.json:32"] = "NYI", + ["not.json:6"] = "NYI", ["refRemote.json"] = "DEFINITELY NYI", ["required.json:0:2"] = "distinguishing objects from arrays", + ["type.json:0:1"] = "1.0 is not an integer!", ["type.json:3:4"] = "distinguishing objects from arrays", ["type.json:3:6"] = "null is weird", ["type.json:4:3"] = "distinguishing objects from arrays", @@ -64,11 +65,12 @@ ["type.json:9:6"] = "null is weird", ["unevaluatedItems.json"] = "NYI", ["unevaluatedProperties.json"] = "NYI", - ["uniqueItems.json:0:11"] = "deepcompare", - ["uniqueItems.json:0:13"] = "deepcompare", + ["uniqueItems.json:0:10"] = "deepcompare", + ["uniqueItems.json:0:12"] = "deepcompare", ["uniqueItems.json:0:14"] = "deepcompare", - ["uniqueItems.json:0:22"] = "deepcompare", - ["uniqueItems.json:0:24"] = "deepcompare", + ["uniqueItems.json:0:15"] = "deepcompare", + ["uniqueItems.json:0:23"] = "deepcompare", + ["uniqueItems.json:0:25"] = "deepcompare", ["uniqueItems.json:0:9"] = "deepcompare", ["unknownKeyword.json"] = "NYI", ["vocabulary.json"] = "NYI", diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_jwt_spec.lua --- a/spec/util_jwt_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_jwt_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,12 @@ local jwt = require "util.jwt"; +local test_keys = require "spec.inputs.test_keys"; + +local array = require "util.array"; +local iter = require "util.iterators"; +local set = require "util.set"; + +-- Ignore long lines. We have some long tokens embedded here. +--luacheck: ignore 631 describe("util.jwt", function () it("validates", function () @@ -8,6 +16,9 @@ local ok, parsed = jwt.verify(key, token); assert.truthy(ok) assert.same({ payload = "this" }, parsed); + + + end); it("rejects invalid", function () local key = "secret"; @@ -16,5 +27,233 @@ local ok = jwt.verify(key, token); assert.falsy(ok) end); + + local function jwt_reference_token(token) + return { + name = "jwt.io reference"; + token; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + end + + local untested_algorithms = set.new(array.collect(iter.keys(jwt._algorithms))); + + local test_cases = { + { + algorithm = "HS256"; + keys = { + { "your-256-bit-secret", "your-256-bit-secret" }; + { "another-secret", "another-secret" }; + }; + + jwt_reference_token [[eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhZG1pbiI6dHJ1ZX0.F-cvL2RcfQhUtCavIM7q7zYE8drmj2LJk0JRkrS6He4]]; + }; + { + algorithm = "HS384"; + keys = { + { "your-384-bit-secret", "your-384-bit-secret" }; + { "another-secret", "another-secret" }; + }; + + jwt_reference_token [[eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.bQTnz6AuMJvmXXQsVPrxeQNvzDkimo7VNXxHeSBfClLufmCVZRUuyTwJF311JHuh]]; + }; + { + algorithm = "HS512"; + keys = { + { "your-512-bit-secret", "your-512-bit-secret" }; + { "another-secret", "another-secret" }; + }; + + jwt_reference_token [[eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.VFb0qJ1LRg_4ujbZoRMXnVkUgiuKq5KxWqNdbKq_G9Vvz-S1zZa9LPxtHWKa64zDl2ofkT8F6jBt_K4riU-fPg]]; + }; + { + algorithm = "ES256"; + keys = { + { test_keys.ecdsa_private_pem, test_keys.ecdsa_public_pem }; + { test_keys.alt_ecdsa_private_pem, test_keys.alt_ecdsa_public_pem }; + }; + { + name = "jwt.io reference"; + [[eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + }; + { + algorithm = "ES512"; + keys = { + { test_keys.ecdsa_521_private_pem, test_keys.ecdsa_521_public_pem }; + { test_keys.alt_ecdsa_521_private_pem, test_keys.alt_ecdsa_521_public_pem }; + }; + { + name = "jwt.io reference"; + [[eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AbVUinMiT3J_03je8WTOIl-VdggzvoFgnOsdouAs-DLOtQzau9valrq-S6pETyi9Q18HH-EuwX49Q7m3KC0GuNBJAc9Tksulgsdq8GqwIqZqDKmG7hNmDzaQG1Dpdezn2qzv-otf3ZZe-qNOXUMRImGekfQFIuH_MjD2e8RZyww6lbZk]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + }; + { + algorithm = "RS256"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + { + name = "jwt.io reference"; + [[eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ]]; + { -- payload + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + }; + }; + { + algorithm = "RS384"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.o1hC1xYbJolSyh0-bOY230w22zEQSk5TiBfc-OCvtpI2JtYlW-23-8B48NpATozzMHn0j3rE0xVUldxShzy0xeJ7vYAccVXu2Gs9rnTVqouc-UZu_wJHkZiKBL67j8_61L6SXswzPAQu4kVDwAefGf5hyYBUM-80vYZwWPEpLI8K4yCBsF6I9N1yQaZAJmkMp_Iw371Menae4Mp4JusvBJS-s6LrmG2QbiZaFaxVJiW8KlUkWyUCns8-qFl5OMeYlgGFsyvvSHvXCzQrsEXqyCdS4tQJd73ayYA4SPtCb9clz76N1zE5WsV4Z0BYrxeb77oA7jJhh994RAPzCG0hmQ]]; + }; + { + algorithm = "RS512"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.jYW04zLDHfR1v7xdrW3lCGZrMIsVe0vWCfVkN2DRns2c3MN-mcp_-RE6TN9umSBYoNV-mnb31wFf8iun3fB6aDS6m_OXAiURVEKrPFNGlR38JSHUtsFzqTOj-wFrJZN4RwvZnNGSMvK3wzzUriZqmiNLsG8lktlEn6KA4kYVaM61_NpmPHWAjGExWv7cjHYupcjMSmR8uMTwN5UuAwgW6FRstCJEfoxwb0WKiyoaSlDuIiHZJ0cyGhhEmmAPiCwtPAwGeaL1yZMcp0p82cpTQ5Qb-7CtRov3N4DcOHgWYk6LomPR5j5cCkePAz87duqyzSMpCB0mCOuE3CU2VMtGeQ]]; + }; + { + algorithm = "PS256"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg]]; + }; + { + algorithm = "PS384"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJQUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.Lfe_aCQme_gQpUk9-6l9qesu0QYZtfdzfy08w8uqqPH_gnw-IVyQwyGLBHPFBJHMbifdSMxPjJjkCD0laIclhnBhowILu6k66_5Y2z78GHg8YjKocAvB-wSUiBhuV6hXVxE5emSjhfVz2OwiCk2bfk2hziRpkdMvfcITkCx9dmxHU6qcEIsTTHuH020UcGayB1-IoimnjTdCsV1y4CMr_ECDjBrqMdnontkqKRIM1dtmgYFsJM6xm7ewi_ksG_qZHhaoBkxQ9wq9OVQRGiSZYowCp73d2BF3jYMhdmv2JiaUz5jRvv6lVU7Quq6ylVAlSPxeov9voYHO1mgZFCY1kQ]]; + }; + { + algorithm = "PS512"; + keys = { + { test_keys.rsa_private_pem, test_keys.rsa_public_pem }; + { test_keys.alt_rsa_private_pem, test_keys.alt_rsa_public_pem }; + }; + + jwt_reference_token [[eyJhbGciOiJQUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.J5W09-rNx0pt5_HBiydR-vOluS6oD-RpYNa8PVWwMcBDQSXiw6-EPW8iSsalXPspGj3ouQjAnOP_4-zrlUUlvUIt2T79XyNeiKuooyIFvka3Y5NnGiOUBHWvWcWp4RcQFMBrZkHtJM23sB5D7Wxjx0-HFeNk-Y3UJgeJVhg5NaWXypLkC4y0ADrUBfGAxhvGdRdULZivfvzuVtv6AzW6NRuEE6DM9xpoWX_4here-yvLS2YPiBTZ8xbB3axdM99LhES-n52lVkiX5AWg2JJkEROZzLMpaacA_xlbUz_zbIaOaoqk8gB5oO7kI6sZej3QAdGigQy-hXiRnW_L98d4GQ]]; + }; + }; + + local function do_verify_test(algorithm, verifying_key, token, expect_payload) + local verify = jwt.new_verifier(algorithm, verifying_key); + + assert.is_string(token); + local result = {verify(token)}; + if expect_payload then + assert.same({ + true; -- success + expect_payload; -- payload + }, result); + else + assert.same({ + false; + "signature-mismatch"; + }, result); + end + end + + local function do_sign_verify_test(algorithm, signing_key, verifying_key, expect_success, expect_token) + local sign = jwt.new_signer(algorithm, signing_key); + + local test_payload = { + sub = "1234567890"; + name = "John Doe"; + admin = true; + iat = 1516239022; + }; + + local token = sign(test_payload); + + if expect_token then + assert.equal(expect_token, token); + end + + do_verify_test(algorithm, verifying_key, token, expect_success and test_payload or false); + end + + + for _, algorithm_tests in ipairs(test_cases) do + local algorithm = algorithm_tests.algorithm; + local keypairs = algorithm_tests.keys; + + untested_algorithms:remove(algorithm); + + describe(algorithm, function () + describe("can do basic sign and verify", function () + for keypair_n, keypair in ipairs(keypairs) do + local signing_key, verifying_key = keypair[1], keypair[2]; + it(("(test key pair %d)"):format(keypair_n), function () + do_sign_verify_test(algorithm, signing_key, verifying_key, true); + end); + end + end); + + if #keypairs >= 2 then + it("rejects invalid tokens", function () + do_sign_verify_test(algorithm, keypairs[1][1], keypairs[2][2], false); + end); + else + pending("rejects invalid tokens", function () + error("Needs at least 2 key pairs"); + end); + end + + if #algorithm_tests > 0 then + for test_n, test_case in ipairs(algorithm_tests) do + it("can verify "..(test_case.name or (("test case %d"):format(test_n))), function () + do_verify_test( + algorithm, + test_case.verifying_key or keypairs[1][2], + test_case[1], + test_case[2] + ); + end); + end + else + pending("can verify reference tokens", function () + error("No test tokens provided"); + end); + end + end); + end + + for algorithm in untested_algorithms do + pending(algorithm.." tests", function () end); + end end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_paseto_spec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/util_paseto_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,292 @@ +-- Ignore long lines in this file +--luacheck: ignore 631 + +describe("util.paseto", function () + local paseto = require "util.paseto"; + local json = require "util.json"; + local hex = require "util.hex"; + + describe("v3.local", function () + local function parse_test_cases(json_test_cases) + local input_cases = json.decode(json_test_cases); + local output_cases = {}; + for _, case in ipairs(input_cases) do + assert.is_string(case.name, "Bad test case: expected name"); + assert.is_nil(output_cases[case.name], "Bad test case: duplicate name"); + output_cases[case.name] = function () + local key = hex.decode(case.key); + local payload, err = paseto.v3_local.decrypt(case.token, key, case.footer, case["implicit-assertion"]); + if case["expect-fail"] then + assert.is_nil(payload); + else + assert.is_nil(err); + assert.same(json.decode(case.payload), payload); + end + end; + end + return output_cases; + end + + local test_cases = parse_test_cases [=[[ + { + "name": "3-E-1", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeg", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-2", + "expect-fail": false, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAqhWxBMDgyBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9IzZfaZpReVpHlDSwfuygx1riVXYVs-UjcrG_apl9oz3jCVmmJbRuKn5ZfD8mHz2db0A", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-3", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlxnt5xyhQjFJomwnt7WW_7r2VT0G704ifult011-TgLCyQ2X8imQhniG_hAQ4BydM", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-4", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlBZa_gOpVj4gv0M9lV6Pwjp8JS_MmaZaTA1LLTULXybOBZ2S4xMbYqYmDRhh3IgEk", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-E-5", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "3-E-6", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmSeEMphEWHiwtDKJftg41O1F8Hat-8kQ82ZIAMFqkx9q5VkWlxZke9ZzMBbb3Znfo.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + }, + { + "name": "3-E-7", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJkzWACWAIoVa0bz7EWSBoTEnS8MvGBYHHo6t6mJunPrFR9JKXFCc0obwz5N-pxFLOc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a secret message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "{\"test-vector\":\"3-E-7\"}" + }, + { + "name": "3-E-8", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJmZHSSKYR6AnPYJV6gpHtx6dLakIG_AOPhu8vKexNyrv5_1qoom6_NaPGecoiz6fR8.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "{\"test-vector\":\"3-E-8\"}" + }, + { + "name": "3-E-9", + "expect-fail": false, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0X-4P3EcxGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlk1nli0_wijTH_vCuRwckEDc82QWK8-lG2fT9wQF271sgbVRVPjm0LwMQZkvvamqU.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": "{\"data\":\"this is a hidden message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"3-E-9\"}" + }, + { + "name": "3-F-3", + "expect-fail": true, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v4.local.1JgN1UG8TFAYS49qsx8rxlwh-9E4ONUm3slJXYi5EibmzxpF0Q-du6gakjuyKCBX8TvnSLOKqCPu8Yh3WSa5yJWigPy33z9XZTJF2HQ9wlLDPtVn_Mu1pPxkTU50ZaBKblJBufRA.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24", + "payload": null, + "footer": "arbitrary-string-that-isn't-json", + "implicit-assertion": "{\"test-vector\":\"3-F-3\"}" + }, + { + "name": "3-F-4", + "expect-fail": true, + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "nonce": "0000000000000000000000000000000000000000000000000000000000000000", + "token": "v3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADbfcIURX_0pVZVU1mAESUzrKZAsRm2EsD6yBoZYn6cpVZNzSJOhSDN-sRaWjfLU-yn9OJH1J_B8GKtOQ9gSQlb8yk9Iza7teRdkiR89ZFyvPPsVjjFiepFUVcMa-LP18zV77f_crJrVXWa5PDNRkCSeHfBBeh", + "payload": null, + "footer": "", + "implicit-assertion": "" + }, + { + "name": "3-F-5", + "expect-fail": true, + "nonce": "26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2", + "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", + "token": "v3.local.JvdVM1RIKh2R1HhGJ4VLjaa4BCp5ZlI8K0BOjbvn9_LwY78vQnDait-Q-sjhF88dG2B0ROIIykcrGHn8wzPbTrqObHhyoKpjy3cwZQzLdiwRsdEK5SDvl02_HjWKJW2oqGMOQJlkYSIbXOgVuIQL65UMdW9WcjOpmqvjqD40NNzed-XPqn1T3w-bJvitYpUJL_rmihc=.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9", + "payload": null, + "footer": "{\"kid\":\"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo\"}", + "implicit-assertion": "" + } + ]]=]; + for name, test in pairs(test_cases) do + it("test case "..name, test); + end + + describe("basic sign/verify", function () + local key = paseto.v3_local.new_key(); + local sign, verify = paseto.v3_local.init(key); + + --luacheck: ignore 211/sign2 + local key2 = paseto.v3_local.new_key(); + local sign2, verify2 = paseto.v3_local.init(key2); + + it("works", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + + local tok = sign(payload); + assert.same(payload, verify(tok)); + assert.is_nil(verify2(tok)); + end); + + it("rejects tokens if implicit assertion fails", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + local tok = sign(payload, nil, "my-custom-assertion"); + assert.is_nil(verify(tok, nil, "my-incorrect-assertion")); + assert.is_nil(verify(tok, nil, nil)); + assert.same(payload, verify(tok, nil, "my-custom-assertion")); + end); + end); + end); + + describe("v4.public", function () + local function parse_test_cases(json_test_cases) + local input_cases = json.decode(json_test_cases); + local output_cases = {}; + for _, case in ipairs(input_cases) do + assert.is_string(case.name, "Bad test case: expected name"); + assert.is_nil(output_cases[case.name], "Bad test case: duplicate name"); + output_cases[case.name] = function () + local verify_key = paseto.v4_public.import_public_key(case["public-key-pem"]); + local payload, err = paseto.v4_public.verify(case.token, verify_key, case.footer, case["implicit-assertion"]); + if case["expect-fail"] then + assert.is_nil(payload); + else + assert.is_nil(err); + assert.same(json.decode(case.payload), payload); + end + end; + end + return output_cases; + end + + local test_cases = parse_test_cases [=[[ + { + "name": "4-S-1", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9bg_XBBzds8lTZShVlwwKSgeKpLT3yukTw6JUz3W4h_ExsQV-P0V54zemZDcAxFaSeef1QlXEFtkqxT1ciiQEDA", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "", + "implicit-assertion": "" + }, + { + "name": "4-S-2", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "" + }, + { + "name": "4-S-3", + "expect-fail": false, + "public-key": "1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2", + "secret-key-seed": "b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a3774", + "secret-key-pem": "-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----", + "public-key-pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----", + "token": "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9NPWciuD3d0o5eXJXG5pJy-DiVEoyPYWs1YSTwWHNJq6DZD3je5gf-0M4JR9ipdUSJbIovzmBECeaWmaqcaP0DQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9", + "payload": "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}", + "footer": "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}", + "implicit-assertion": "{\"test-vector\":\"4-S-3\"}" + }]]=]; + for name, test in pairs(test_cases) do + it("test case "..name, test); + end + + describe("basic sign/verify", function () + local function new_keypair() + local kp = paseto.v4_public.new_keypair(); + return kp:private_pem(), kp:public_pem(); + end + + local privkey1, pubkey1 = new_keypair(); + local privkey2, pubkey2 = new_keypair(); + local sign1, verify1 = paseto.v4_public.init(privkey1, pubkey1); + local sign2, verify2 = paseto.v4_public.init(privkey2, pubkey2); + + it("works", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + + local tok1 = sign1(payload); + assert.same(payload, verify1(tok1)); + assert.is_nil(verify2(tok1)); + + local tok2 = sign2(payload); + assert.same(payload, verify2(tok2)); + assert.is_nil(verify1(tok2)); + end); + + it("rejects tokens if implicit assertion fails", function () + local payload = { foo = "hello world", b = { 1, 2, 3 } }; + local tok = sign1(payload, nil, "my-custom-assertion"); + assert.is_nil(verify1(tok, nil, "my-incorrect-assertion")); + assert.is_nil(verify1(tok, nil, nil)); + assert.same(payload, verify1(tok, nil, "my-custom-assertion")); + end); + end); + end); + + describe("pae", function () + it("encodes correctly", function () + -- These test cases are taken from the PASETO docs + -- https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Common.md + assert.equal("\x00\x00\x00\x00\x00\x00\x00\x00", paseto.pae{}); + assert.equal("\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", paseto.pae{''}); + assert.equal("\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00test", paseto.pae{'test'}); + assert.has_errors(function () + paseto.pae("test"); + end); + end); + end); +end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_poll_spec.lua --- a/spec/util_poll_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_poll_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,35 @@ -describe("util.poll", function () - it("loads", function () - require "util.poll" +describe("util.poll", function() + local poll; + setup(function() + poll = require "util.poll"; + end); + it("loads", function() + assert.is_table(poll); + assert.is_function(poll.new); + assert.is_string(poll.api); end); + describe("new", function() + local p; + setup(function() + p = poll.new(); + end) + it("times out", function () + local fd, err = p:wait(0); + assert.falsy(fd); + assert.equal("timeout", err); + end); + it("works", function() + -- stdout should be writable, right? + assert.truthy(p:add(1, false, true)); + local fd, r, w = p:wait(1); + assert.is_number(fd); + assert.is_boolean(r); + assert.is_boolean(w); + assert.equal(1, fd); + assert.falsy(r); + assert.truthy(w); + assert.truthy(p:del(1)); + end); + end) end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_promise_spec.lua --- a/spec/util_promise_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_promise_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,6 +7,11 @@ assert(promise.new()); end); end); + it("supplies a sensible tostring()", function () + local s = tostring(promise.new()); + assert.truthy(s:find("promise", 1, true)); + assert.truthy(s:find("pending", 1, true)); + end); it("notifies immediately for fulfilled promises", function () local p = promise.new(function (resolve) resolve("foo"); @@ -30,6 +35,27 @@ r("foo"); assert.spy(cb).was_called(1); end); + it("ignores resolve/reject of settled promises", function () + local res, rej; + local p = promise.new(function (resolve, reject) + res, rej = resolve, reject; + end); + local cb = spy.new(function (v) + assert.equal("foo", v); + end); + p:next(cb, cb); + assert.spy(cb).was_called(0); + res("foo"); + assert.spy(cb).was_called(1); + rej("bar"); + assert.spy(cb).was_called(1); + rej(promise.resolve("bar")); + assert.spy(cb).was_called(1); + res(promise.reject("bar")); + assert.spy(cb).was_called(1); + res(promise.resolve("bar")); + assert.spy(cb).was_called(1); + end); it("allows chaining :next() calls", function () local r; local result; @@ -438,6 +464,26 @@ { status = "rejected", reason = "this fails" }; }, result); end); + it("works when all promises reject", function () + local r1, r2; + local p1, p2 = promise.new(function (_, reject) r1 = reject end), promise.new(function (_, reject) r2 = reject end); + local p = promise.all_settled({ p1, p2 }); + + local result; + local cb = spy.new(function (v) + result = v; + end); + p:next(cb); + assert.spy(cb).was_called(0); + r2("this fails"); + assert.spy(cb).was_called(0); + r1("this fails too"); + assert.spy(cb).was_called(1); + assert.same({ + { status = "rejected", reason = "this fails too" }; + { status = "rejected", reason = "this fails" }; + }, result); + end); it("works with non-numeric keys", function () local r1, r2; local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_rfc6724_spec.lua --- a/spec/util_rfc6724_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ - -local rfc6724 = require "util.rfc6724"; -local new_ip = require"util.ip".new_ip; - -describe("util.rfc6724", function() - describe("#source()", function() - it("should work", function() - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, - "2001:db8:3::1", - "prefer appropriate scope"); - assert.are.equal(rfc6724.source(new_ip("ff05::1", "IPv6"), - {new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::1", "IPv6")}).addr, - "2001:db8:3::1", - "prefer appropriate scope"); - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:2::1", "IPv6")}).addr, - "2001:db8:1::1", - "prefer same address"); -- "2001:db8:1::1" should be marked "deprecated" here, we don't handle that right now - assert.are.equal(rfc6724.source(new_ip("fe80::1", "IPv6"), - {new_ip("fe80::2", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}).addr, - "fe80::2", - "prefer appropriate scope"); -- "fe80::2" should be marked "deprecated" here, we don't handle that right now - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, - "2001:db8:1::2", - "longest matching prefix"); - --[[ "2001:db8:1::2" should be a care-of address and "2001:db8:3::2" a home address, we can't handle this and would fail - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::1", "IPv6"), - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::2", "IPv6")}).addr, - "2001:db8:3::2", - "prefer home address"); - ]] - assert.are.equal(rfc6724.source(new_ip("2002:c633:6401::1", "IPv6"), - {new_ip("2002:c633:6401::d5e3:7953:13eb:22e8", "IPv6"), new_ip("2001:db8:1::2", "IPv6")}).addr, - "2002:c633:6401::d5e3:7953:13eb:22e8", - "prefer matching label"); -- "2002:c633:6401::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now - assert.are.equal(rfc6724.source(new_ip("2001:db8:1::d5e3:0:0:1", "IPv6"), - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:1::d5e3:7953:13eb:22e8", "IPv6")}).addr, - "2001:db8:1::d5e3:7953:13eb:22e8", - "prefer temporary address") -- "2001:db8:1::2" should be marked "public" and "2001:db8:1::d5e3:7953:13eb:22e8" should be marked "temporary" here, we don't handle that right now - end); - end); - describe("#destination()", function() - it("should work", function() - local order; - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("169.254.13.78", "IPv4")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer matching scope"); - assert.are.equal(order[2].addr, "198.51.100.121", "prefer matching scope"); - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("198.51.100.121", "IPv4")}, - {new_ip("fe80::1", "IPv6"), new_ip("198.51.100.117", "IPv4")}) - assert.are.equal(order[1].addr, "198.51.100.121", "prefer matching scope"); - assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching scope"); - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("10.1.2.3", "IPv4")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::1", "IPv6"), new_ip("10.1.2.4", "IPv4")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); - assert.are.equal(order[2].addr, "10.1.2.3", "prefer higher precedence"); - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "fe80::1", "prefer smaller scope"); - assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer smaller scope"); - - --[[ "2001:db8:1::2" and "fe80::2" should be marked "care-of address", while "2001:db8:3::1" should be marked "home address", we can't currently handle this and would fail the test - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3::1", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer home address"); - assert.are.equal(order[2].addr, "fe80::1", "prefer home address"); - ]] - - --[[ "fe80::2" should be marked "deprecated", we can't currently handle this and would fail the test - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("fe80::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "avoid deprecated addresses"); - assert.are.equal(order[2].addr, "fe80::1", "avoid deprecated addresses"); - ]] - - order = rfc6724.destination({new_ip("2001:db8:1::1", "IPv6"), new_ip("2001:db8:3ffe::1", "IPv6")}, - {new_ip("2001:db8:1::2", "IPv6"), new_ip("2001:db8:3f44::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "longest matching prefix"); - assert.are.equal(order[2].addr, "2001:db8:3ffe::1", "longest matching prefix"); - - order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, - {new_ip("2002:c633:6401::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2002:c633:6401::1", "prefer matching label"); - assert.are.equal(order[2].addr, "2001:db8:1::1", "prefer matching label"); - - order = rfc6724.destination({new_ip("2002:c633:6401::1", "IPv6"), new_ip("2001:db8:1::1", "IPv6")}, - {new_ip("2002:c633:6401::2", "IPv6"), new_ip("2001:db8:1::2", "IPv6"), new_ip("fe80::2", "IPv6")}) - assert.are.equal(order[1].addr, "2001:db8:1::1", "prefer higher precedence"); - assert.are.equal(order[2].addr, "2002:c633:6401::1", "prefer higher precedence"); - end); - end); -end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_roles_spec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/util_roles_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,134 @@ +describe("util.roles", function () + randomize(false); + local roles; + it("can be loaded", function () + roles = require "util.roles"; + end); + local test_role; + it("can create a new role", function () + test_role = roles.new(); + assert.is_not_nil(test_role); + assert.is_truthy(roles.is_role(test_role)); + end); + describe("role object", function () + it("can be initialized with permissions", function () + local test_role_2 = roles.new({ + permissions = { + perm1 = true; + perm2 = false; + }; + }); + assert.truthy(test_role_2:may("perm1")); + assert.falsy(test_role_2:may("perm2")); + end); + it("has a sensible tostring", function () + local test_role_2 = roles.new({ + id = "test-role-2"; + name = "Test Role 2"; + }); + assert.truthy(tostring(test_role_2):find(test_role_2.id, 1, true)); + assert.truthy(tostring(test_role_2):find("Test Role 2", 1, true)); + end); + it("is restrictive by default", function () + assert.falsy(test_role:may("my-permission")); + end); + it("allows you to set permissions", function () + test_role:set_permission("my-permission", true); + assert.truthy(test_role:may("my-permission")); + end); + it("allows you to set negative permissions", function () + test_role:set_permission("my-other-permission", false); + assert.falsy(test_role:may("my-other-permission")); + end); + it("does not allows you to override previously set permissions by default", function () + local ok, err = test_role:set_permission("my-permission", false); + assert.falsy(ok); + assert.is_equal("policy-already-exists", err); + -- Confirm old permission still in place + assert.truthy(test_role:may("my-permission")); + end); + it("allows you to explicitly override previously set permissions", function () + assert.truthy(test_role:set_permission("my-permission", false, true)); + assert.falsy(test_role:may("my-permission")); + end); + describe("inheritance", function () + local child_role; + it("works", function () + test_role:set_permission("inherited-permission", true); + child_role = roles.new({ + inherits = { test_role }; + }); + assert.truthy(child_role:may("inherited-permission")); + assert.falsy(child_role:may("my-permission")); + end); + it("allows listing policies", function () + local expected = { + ["my-permission"] = false; + ["my-other-permission"] = false; + ["inherited-permission"] = true; + }; + local received = {}; + for permission_name, permission_policy in child_role:policies() do + received[permission_name] = permission_policy; + end + assert.same(expected, received); + end); + it("supports multiple depths of inheritance", function () + local grandchild_role = roles.new({ + inherits = { child_role }; + }); + assert.truthy(grandchild_role:may("inherited-permission")); + end); + describe("supports ordered inheritance from multiple roles", function () + local parent_role = roles.new(); + local final_role = roles.new({ + -- Yes, the names are getting confusing. + -- btw, test_role is inherited through child_role. + inherits = { parent_role, child_role }; + }); + + local test_cases = { + -- { , , } + { true, nil, false, result = true }; + { nil, false, true, result = false }; + { nil, true, false, result = true }; + { nil, nil, false, result = false }; + { nil, nil, true, result = true }; + }; + + for n, test_case in ipairs(test_cases) do + it("(case "..n..")", function () + local perm_name = ("multi-inheritance-perm-%d"):format(n); + assert.truthy(final_role:set_permission(perm_name, test_case[1])); + assert.truthy(parent_role:set_permission(perm_name, test_case[2])); + assert.truthy(test_role:set_permission(perm_name, test_case[3])); + assert.equal(test_case.result, final_role:may(perm_name)); + end); + end + end); + it("updates child roles when parent roles change", function () + assert.truthy(child_role:may("inherited-permission")); + assert.truthy(test_role:set_permission("inherited-permission", false, true)); + assert.falsy(child_role:may("inherited-permission")); + end); + end); + describe("cloning", function () + local cloned_role; + it("works", function () + assert.truthy(test_role:set_permission("perm-1", true)); + cloned_role = test_role:clone(); + assert.truthy(cloned_role:may("perm-1")); + end); + it("isolates changes", function () + -- After cloning, changes in either the original or the clone + -- should not appear in the other. + assert.truthy(test_role:set_permission("perm-1", false, true)); + assert.truthy(test_role:set_permission("perm-2", true)); + assert.truthy(cloned_role:set_permission("perm-3", true)); + assert.truthy(cloned_role:may("perm-1")); + assert.falsy(cloned_role:may("perm-2")); + assert.falsy(test_role:may("perm-3")); + end); + end); + end); +end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_sasl_spec.lua --- a/spec/util_sasl_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_sasl_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -39,5 +39,37 @@ -- TODO SCRAM end); + + describe("oauthbearer profile", function() + local profile = { + oauthbearer = function(_, token, _realm, _authzid) + if token == "example-bearer-token" then + return "user", true, {}; + else + return nil, nil, {} + end + end; + } + + it("works with OAUTHBEARER", function() + local bearer = sasl.new("sasl.test", profile); + + assert.truthy(bearer:select("OAUTHBEARER")); + assert.equals("success", bearer:process("n,,\1auth=Bearer example-bearer-token\1\1")); + assert.equals("user", bearer.username); + end) + + + it("returns extras with OAUTHBEARER", function() + local bearer = sasl.new("sasl.test", profile); + + assert.truthy(bearer:select("OAUTHBEARER")); + local status, extra = bearer:process("n,,\1auth=Bearer unknown\1\1"); + assert.equals("challenge", status); + assert.equals("{\"status\":\"invalid_token\"}", extra); + assert.equals("failure", bearer:process("\1")); + end) + + end) end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_smqueue_spec.lua --- a/spec/util_smqueue_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_smqueue_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -5,6 +5,9 @@ describe("#new()", function() it("should work", function() + assert.has_error(function () smqueue.new(-1) end); + assert.has_error(function () smqueue.new(0) end); + assert.not_has_error(function () smqueue.new(1) end); local q = smqueue.new(10); assert.truthy(q); end) diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_stanza_spec.lua --- a/spec/util_stanza_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_stanza_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -314,6 +314,20 @@ end) end) + describe("#add_error()", function () + describe("basics", function () + local s = st.stanza("custom", { xmlns = "urn:example:foo" }); + local e = s:add_error("cancel", "not-acceptable", "UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!") + :tag("dungeon", { xmlns = "urn:uuid:c9026187-5b05-4e70-b265-c3b6338a7d0f", period="1000000years"}); + assert.equal(s, e); + local typ, cond, text, extra = e:get_error(); + assert.equal("cancel", typ); + assert.equal("not-acceptable", cond); + assert.equal("UNACCEPTABLE!!!! ONE MILLION YEARS DUNGEON!", text); + assert.is_nil(extra); + end) + end) + describe("should reject #invalid", function () local invalid_names = { ["empty string"] = "", ["characters"] = "<>"; diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_strbitop.lua --- a/spec/util_strbitop.lua Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -local strbitop = require "util.strbitop"; -describe("util.strbitop", function () - describe("sand()", function () - it("works", function () - assert.equal(string.rep("Aa", 100), strbitop.sand(string.rep("a", 200), "Aa")); - end); - it("returns empty string if first argument is empty", function () - assert.equal("", strbitop.sand("", "")); - assert.equal("", strbitop.sand("", "key")); - end); - it("returns initial string if key is empty", function () - assert.equal("hello", strbitop.sand("hello", "")); - end); - end); - - describe("sor()", function () - it("works", function () - assert.equal(string.rep("a", 200), strbitop.sor(string.rep("Aa", 100), "a")); - end); - it("returns empty string if first argument is empty", function () - assert.equal("", strbitop.sor("", "")); - assert.equal("", strbitop.sor("", "key")); - end); - it("returns initial string if key is empty", function () - assert.equal("hello", strbitop.sor("hello", "")); - end); - end); - - describe("sxor()", function () - it("works", function () - assert.equal(string.rep("Aa", 100), strbitop.sxor(string.rep("a", 200), " \0")); - end); - it("returns empty string if first argument is empty", function () - assert.equal("", strbitop.sxor("", "")); - assert.equal("", strbitop.sxor("", "key")); - end); - it("returns initial string if key is empty", function () - assert.equal("hello", strbitop.sxor("hello", "")); - end); - end); -end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_strbitop_spec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/util_strbitop_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,85 @@ +local strbitop = require "util.strbitop"; +describe("util.strbitop", function () + describe("sand()", function () + it("works", function () + assert.equal(string.rep("Aa", 100), strbitop.sand(string.rep("a", 200), "Aa")); + end); + it("returns empty string if first argument is empty", function () + assert.equal("", strbitop.sand("", "")); + assert.equal("", strbitop.sand("", "key")); + end); + it("returns initial string if key is empty", function () + assert.equal("hello", strbitop.sand("hello", "")); + end); + end); + + describe("sor()", function () + it("works", function () + assert.equal(string.rep("a", 200), strbitop.sor(string.rep("Aa", 100), "a")); + end); + it("returns empty string if first argument is empty", function () + assert.equal("", strbitop.sor("", "")); + assert.equal("", strbitop.sor("", "key")); + end); + it("returns initial string if key is empty", function () + assert.equal("hello", strbitop.sor("hello", "")); + end); + end); + + describe("sxor()", function () + it("works", function () + assert.equal(string.rep("Aa", 100), strbitop.sxor(string.rep("a", 200), " \0")); + end); + it("returns empty string if first argument is empty", function () + assert.equal("", strbitop.sxor("", "")); + assert.equal("", strbitop.sxor("", "key")); + end); + it("returns initial string if key is empty", function () + assert.equal("hello", strbitop.sxor("hello", "")); + end); + end); + + describe("common_prefix_bits()", function () + local function B(s) + assert(#s%8==0, "Invalid test input: B(s): s should be a multiple of 8 bits in length"); + local byte = 0; + local out_str = {}; + for i = 1, #s do + local bit_ascii = s:byte(i); + if bit_ascii == 49 then -- '1' + byte = byte + 2^((7-(i-1))%8); + elseif bit_ascii ~= 48 then + error("Invalid test input: B(s): s should contain only '0' or '1' characters"); + end + if (i-1)%8 == 7 then + table.insert(out_str, string.char(byte)); + byte = 0; + end + end + return table.concat(out_str); + end + + local _cpb = strbitop.common_prefix_bits; + local function test(a, b) + local Ba, Bb = B(a), B(b); + local ret1 = _cpb(Ba, Bb); + local ret2 = _cpb(Bb, Ba); + assert(ret1 == ret2, ("parameter order should not make a difference to the result (%s, %s) = %d, reversed = %d"):format(a, b, ret1, ret2)); + return ret1; + end + + it("works on single bytes", function () + assert.equal(0, test("00000000", "11111111")); + assert.equal(1, test("10000000", "11111111")); + assert.equal(0, test("01000000", "11111111")); + assert.equal(0, test("01000000", "11111111")); + assert.equal(8, test("11111111", "11111111")); + end); + + it("works on multiple bytes", function () + for i = 0, 16 do + assert.equal(i, test(string.rep("1", i)..string.rep("0", 16-i), "1111111111111111")); + end + end); + end); +end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_table_spec.lua --- a/spec/util_table_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_table_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,6 +12,48 @@ assert.same({ "lorem", "ipsum", "dolor", "sit", "amet", n = 5 }, u_table.pack("lorem", "ipsum", "dolor", "sit", "amet")); end); end); + + describe("move()", function () + it("works", function () + local t1 = { "apple", "banana", "carrot" }; + local t2 = { "cat", "donkey", "elephant" }; + local t3 = {}; + u_table.move(t1, 1, 3, 1, t3); + u_table.move(t2, 1, 3, 3, t3); + assert.same({ "apple", "banana", "cat", "donkey", "elephant" }, t3); + end); + it("supports overlapping regions", function () + do + local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" }; + u_table.move(t1, 1, 3, 3); + assert.same({ "apple", "banana", "apple", "banana", "carrot", "fig", "grapefruit" }, t1); + end + + do + local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" }; + u_table.move(t1, 1, 3, 2); + assert.same({ "apple", "apple", "banana", "carrot", "endive", "fig", "grapefruit" }, t1); + end + + do + local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" }; + u_table.move(t1, 3, 5, 2); + assert.same({ "apple", "carrot", "date", "endive", "endive", "fig", "grapefruit" }, t1); + end + + do + local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" }; + u_table.move(t1, 3, 5, 6); + assert.same({ "apple", "banana", "carrot", "date", "endive", "carrot", "date", "endive" }, t1); + end + + do + local t1 = { "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" }; + u_table.move(t1, 3, 1, 3); + assert.same({ "apple", "banana", "carrot", "date", "endive", "fig", "grapefruit" }, t1); + end + end); + end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_throttle_spec.lua --- a/spec/util_throttle_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_throttle_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,5 @@ +-- luacheck: ignore 411/a -- Mock util.time local now = 0; -- wibbly-wobbly... timey-wimey... stuff diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_uuid_spec.lua --- a/spec/util_uuid_spec.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/spec/util_uuid_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -5,7 +5,7 @@ describe("util.uuid", function() describe("#generate()", function() it("should work follow the UUID pattern", function() - -- https://tools.ietf.org/html/rfc4122#section-4.4 + -- https://www.rfc-editor.org/rfc/rfc4122.html#section-4.4 local pattern = "^" .. table.concat({ string.rep("%x", 8), @@ -20,6 +20,28 @@ for _ = 1, 100 do assert.is_string(uuid.generate():match(pattern)); end + + assert.truthy(uuid.generate() ~= uuid.generate(), "does not generate the same UUIDv4 twice") + end); + end); + describe("#v7", function() + it("should also follow the UUID pattern", function() + local pattern = "^" .. table.concat({ + string.rep("%x", 8), + string.rep("%x", 4), + "7" .. -- version + string.rep("%x", 3), + "[89ab]" .. -- reserved bits of 1 and 0 + string.rep("%x", 3), + string.rep("%x", 12), + }, "%-") .. "$"; + + local one = uuid.v7(); -- one before the loop to ensure some time passes + for _ = 1, 100 do + assert.is_string(uuid.v7():match(pattern)); + end + -- one after the loop when some time should have passed + assert.truthy(one < uuid.v7(), "should be ordererd") end); end); end); diff -r 54a936345aaa -r 5d9ec2e55d74 spec/util_xtemplate_spec.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/spec/util_xtemplate_spec.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,43 @@ +local st = require "prosody.util.stanza"; +local xtemplate = require "prosody.util.xtemplate"; + +describe("util.xtemplate", function () + describe("render()", function () + it("works", function () + assert.same("Hello", xtemplate.render("{greeting}", st.stanza("root"):text_tag("greeting", "Hello")), "regular text content") + assert.same("Hello", xtemplate.render("{#}", st.stanza("root"):text("Hello")), "top tag text content") + assert.same("Hello", xtemplate.render("{greeting/@en}", st.stanza("root"):tag("greeting", { en = "Hello" })), "attribute") + end) + it("supports conditionals", function () + local atom_tmpl = "{@pubsub:title|and{*{@pubsub:title}*\n\n}}{summary|or{{author/name|and{{author/name} posted }}{title}}}"; + local atom_data = st.stanza("entry", { xmlns = "http://www.w3.org/2005/Atom" }); + assert.same("", xtemplate.render(atom_tmpl, atom_data)); + + atom_data:text_tag("title", "an Entry") + assert.same("an Entry", xtemplate.render(atom_tmpl, atom_data)); + + atom_data:tag("author"):text_tag("name","Juliet"):up(); + assert.same("Juliet posted an Entry", xtemplate.render(atom_tmpl, atom_data)); + + atom_data:text_tag("summary", "Juliet just posted a new entry"); + assert.same("Juliet just posted a new entry", xtemplate.render(atom_tmpl, atom_data)); + + atom_data.attr["xmlns:pubsub"] = "http://jabber.org/protocol/pubsub"; + atom_data.attr["pubsub:title"] = "Juliets musings"; + assert.same("*Juliets musings*\n\nJuliet just posted a new entry", xtemplate.render(atom_tmpl, atom_data)); + end) + it("can strip surrounding whitespace", function () + assert.same("Hello ", xtemplate.render(" {-greeting} ", st.stanza("root"):text_tag("greeting", "Hello"))) + assert.same(" Hello", xtemplate.render(" {greeting-} ", st.stanza("root"):text_tag("greeting", "Hello"))) + assert.same("Hello", xtemplate.render(" {-greeting-} ", st.stanza("root"):text_tag("greeting", "Hello"))) + end) + describe("each", function () + it("makes sense", function () + local x = st.stanza("root"):tag("foo"):tag("bar") + for i = 1, 5 do x:text_tag("i", tostring(i)); end + x:reset(); + assert.same("12345", xtemplate.render("{foo/bar|each(i){{#}}}", x)); + end) + end) + end) +end) diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/README.md Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,48 @@ +# Teal definitions and sources + +This directory contains files written in the +[Teal](https://github.com/teal-language/tl) language, a typed dialect of +Lua. There are two kinds of files, `.tl` Teal source code and `.d.tl` +type definitions files for modules written in Lua. The later allows +writing type-aware Teal using regular Lua or C code. + +## Setup + +The Teal compiler can be installed from LuaRocks using: + +```bash +luarocks install tl +``` + +## Checking types + +```bash +tl check teal-src/prosody/util/example.tl +``` + +Some editors and IDEs also have support, see [text editor +support](https://github.com/teal-language/tl#text-editor-support) + + +## Compiling to Lua + +`GNUmakefile` contains a rule for building Lua files from Teal sources. +It also applies [LuaFormat](https://github.com/Koihik/LuaFormatter) to +make the resulting code more readable, albeit this makes the line +numbers no longer match the original Teal source. Sometimes minor +`luacheck` issues remain, such as types being represented as unused +tables, which can be removed. + +```bash +sensible-editor teal-src/prosody/util/example.tl +# Write some code, remember to run tl check +make util/example.lua +sensible-editor util/example.lua +# Apply any minor tweaks that may be needed +``` + +## Files of note + +`module.d.tl` +: Describes the module environment. + diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/module.d.tl --- a/teal-src/module.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ b/teal-src/module.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,4 @@ -local st = require"util.stanza" +local st = require "prosody.util.stanza" global record moduleapi get_name : function (moduleapi) : string @@ -34,8 +34,11 @@ get_option : config_getter get_option_scalar : config_getter get_option_string : config_getter - get_option_number : config_getter + get_option_number : function (moduleapi, string, number, number, number) : number + get_option_integer : function (moduleapi, string, integer, integer, integer) : integer get_option_boolean : config_getter + get_option_enum : function (moduleapi, string, ... : A) : A + get_option_period : function (moduleapi, string|number, string|number, string|number, string|number) : number record util_array -- TODO import def { any } @@ -62,7 +65,12 @@ send_iq : function (moduleapi, st.stanza_t, util_session, number) broadcast : function (moduleapi, { string }, st.stanza_t, function) type timer_callback = function (number, ... : any) : number - add_timer : function (moduleapi, number, timer_callback, ... : any) + record timer_wrapper + stop : function (timer_wrapper) + disarm : function (timer_wrapper) + reschedule : function (timer_wrapper, number) + end + add_timer : function (moduleapi, number, timer_callback, ... : any) : timer_wrapper get_directory : function (moduleapi) : string enum file_mode "r" "w" "a" "r+" "w+" "a+" @@ -70,6 +78,7 @@ load_resource : function (moduleapi, string, file_mode) : FILE enum store_type "keyval" + "keyval+" "map" "archive" end @@ -121,12 +130,20 @@ path : string resource_path : string + -- access control + may : function (moduleapi, string, table|string) + default_permission : function (string, string) + default_permissions : function (string, { string }) + -- methods the module can add load : function () add_host : function (moduleapi) save : function () : any restore : function (any) unload : function () + + -- added by mod_http + http_url : function (moduleapi, string, string, string) : string end global module : moduleapi diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/plugins/mod_cron.tl --- a/teal-src/plugins/mod_cron.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,106 +0,0 @@ -module:set_global(); - -local async = require "util.async"; -local datetime = require "util.datetime"; - -local record map_store - -- TODO move to somewhere sensible - get : function (map_store, string, K) : V - set : function (map_store, string, K, V) -end - -local enum frequency - "hourly" - "daily" - "weekly" -end - -local record task_spec - id : string -- unique id - name : string -- name or short description - when : frequency - last : integer - run : function (task_spec, integer) - save : function (task_spec, integer) -end - -local record task_event - source : module - item : task_spec -end - -local periods : { frequency : integer } = { hourly = 3600, daily = 86400, weekly = 7*86400 } - -local active_hosts : { string : boolean } = { } - -function module.add_host(host_module : moduleapi) - - local last_run_times = host_module:open_store("cron", "map") as map_store; - active_hosts[host_module.host] = true; - - local function save_task(task : task_spec, started_at : integer) - last_run_times:set(nil, task.id, started_at); - end - - local function task_added(event : task_event) : boolean - local task = event.item; - if task.name == nil then - task.name = task.when; - end - if task.id == nil then - task.id = event.source.name .. "/" .. task.name:gsub("%W", "_"):lower(); - end - if task.last == nil then - task.last = last_run_times:get(nil, task.id); - end - task.save = save_task; - module:log("debug", "%s task %s added, last run %s", task.when, task.id, - task.last and datetime.datetime(task.last) or "never"); - if task.last == nil then - -- initialize new tasks so e.g. daily tasks run at ~midnight UTC for now - local now = os.time(); - task.last = now - now % periods[task.when]; - end - return true; - end - - local function task_removed(event : task_event) : boolean - local task = event.item; - host_module:log("debug", "Task %s removed", task.id); - return true; - end - - host_module:handle_items("task", task_added, task_removed, true); - - function host_module.unload() - active_hosts[host_module.host]=nil; - end -end - -local function should_run(when : frequency, last : integer) : boolean - return not last or last + periods[when]*0.995 <= os.time(); -end - -local function run_task(task : task_spec) - local started_at = os.time(); - task:run(started_at); - task.last = started_at; - task:save(started_at); -end - -local task_runner = async.runner(run_task); -module:add_timer(1, function() : integer - module:log("info", "Running periodic tasks"); - local delay = 3600; - for host in pairs(active_hosts) do - module:log("debug", "Running periodic tasks for host %s", host); - for _, task in ipairs(module:context(host):get_host_items("task") as { task_spec } ) do - module:log("debug", "Considering %s task %s (%s)", task.when, task.id, task.run); - if should_run(task.when, task.last) then task_runner:run(task); end - end - end - module:log("debug", "Wait %ds", delay); - return delay; -end); - --- TODO measure load, pick a good time to do stuff diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/core/storagemanager.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/core/storagemanager.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,74 @@ +-- Storage local record API Description +-- +-- This is written as a TypedLua description + +-- Key-Value stores (the default) + +local stanza = require"prosody.util.stanza".stanza_t + +local record keyval_store + get : function ( keyval_store, string ) : any , string + set : function ( keyval_store, string, any ) : boolean, string +end + +-- Map stores (key-key-value stores) + +local record map_store + get : function ( map_store, string, any ) : any, string + set : function ( map_store, string, any, any ) : boolean, string + set_keys : function ( map_store, string, { any : any }) : boolean, string + remove : table +end + +-- Archive stores + +local record archive_query + start : number -- timestamp + ["end"]: number -- timestamp + with : string + after : string -- archive id + before : string -- archive id + total : boolean +end + +local record archive_store + -- Optional set of capabilities + caps : { + -- Optional total count of matching items returned as second return value from :find() + string : any + } + + -- Add to the archive + append : function ( archive_store, string, string, any, number, string ) : string, string + + -- Iterate over archive + type iterator = function () : string, any, number, string + find : function ( archive_store, string, archive_query ) : iterator, integer + + -- Removal of items. API like find. Optional + delete : function ( archive_store, string, archive_query ) : boolean | number, string + + -- Array of dates which do have messages (Optional) + dates : function ( archive_store, string ) : { string }, string + + -- Map of counts per "with" field + summary : function ( archive_store, string, archive_query ) : { string : integer }, string + + -- Map-store API + get : function ( archive_store, string, string ) : stanza, number, string + get : function ( archive_store, string, string ) : nil, string + set : function ( archive_store, string, string, stanza, number, string ) : boolean, string +end + +-- This represents moduleapi +local record coremodule + -- If the first string is omitted then the name of the module is used + -- The second string is one of "keyval" (default), "map" or "archive" + open_store : function (archive_store, string, string) : keyval_store, string + open_store : function (archive_store, string, string) : map_store, string + open_store : function (archive_store, string, string) : archive_store, string + + -- Other module methods omitted +end + +return coremodule diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/core/usermanager.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/core/usermanager.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,46 @@ +local Role = require "prosody.util.roles".Role; + +local record usermanager + record AuthProvider + -- TODO + end + record AccountInfo + created : number + password_updated : any + enabled : boolean + end + + -- Users + test_password : function (username : string, host : string, password : string) : boolean + get_password : function (username : string, host : string) : string, string + set_password : function (username : string, host : string, password : string) : boolean, string + get_account_info : function (username : string, host : string) : AccountInfo + user_exists : function (username : string, host : string) : boolean + create_user : function (username : string, password : string, host : string) : boolean, string + delete_user : function (username : string, host : string) : boolean, string + user_is_enabled : function (username : string, host : string) : boolean, string + enable_user : function (username : string, host : string) : boolean, string + disable_user : function (username : string, host : string) : boolean, string + users : function (host : string) : function () : string + + -- Roles + get_user_role : function (username : string, host : string) : Role + set_user_role : function (username : string, host : string, role_name : string) : boolean, string + user_can_assume_role : function (username : string, host : string, role_name : string) : boolean + add_user_secondary_role : function (username : string, host: string, role_name : string) : boolean, string + remove_user_secondary_role : function (username : string, host: string, role_name : string) : boolean, string + get_user_secondary_roles : function (username : string, host : string) : { string : Role } + get_users_with_role : function (role : string, host : string) : { string } + get_jid_role : function (jid : string, host : string) : Role + set_jid_role : function (jid : string, host : string, role_name : string) : boolean + get_jids_with_role : function (role : string, host : string) : { string } + get_role_by_name : function (role_name : string) : Role + + -- Etc + get_provider : function (host : string) : AuthProvider + get_sasl_handler : function (host : string, session : table) : table + initialize_host : function (host : string) + new_null_provider : function () : AuthProvider +end + +return usermanager diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/net/http.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/net/http.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,86 @@ +local Promise = require "prosody.util.promise".Promise; + +local record sslctx -- from LuaSec +end + +local record lib + + enum http_method + "GET" + "HEAD" + "POST" + "PUT" + "OPTIONS" + "DELETE" + -- etc? + end + + record http_client_options + sslctx : sslctx + end + + record http_options + id : string + onlystatus : boolean + body : string + method : http_method + headers : { string : string } + insecure : boolean + suppress_errors : boolean + streaming_handler : function + suppress_url : boolean + sslctx : sslctx + end + + record http_request + host : string + port : string + enum Scheme + "http" + "https" + end + scheme : Scheme + url : string + userinfo : string + path : string + + method : http_method + headers : { string : string } + + insecure : boolean + suppress_errors : boolean + streaming_handler : function + http : http_client + time : integer + id : string + callback : http_callback + end + + record http_response + end + + type http_callback = function (string, number, http_response, http_request) + + record http_client + options : http_client_options + request : function (http_client, string, http_options, http_callback) + end + + request : function (string, http_options, http_callback) : Promise, string + default : http_client + new : function (http_client_options) : http_client + events : table + -- COMPAT + urlencode : function (string) : string + urldecode : function (string) : string + formencode : function ({ string : string }) : string + formdecode : function (string) : { string : string } + destroy_request : function (http_request) + + enum available_features + "sni" + end + features : { available_features : boolean } +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/net/http/codes.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/net/http/codes.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,2 @@ +local type response_codes = { integer : string } +return response_codes diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/net/http/errors.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/net/http/errors.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,22 @@ +local record http_errors + enum known_conditions + "cancelled" + "connection-closed" + "certificate-chain-invalid" + "certificate-verify-failed" + "connection failed" + "invalid-url" + "unable to resolve service" + end + type registry_keys = known_conditions | integer + record error + type : string + condition : string + code : integer + text : string + end + registry : { registry_keys : error } + new : function (integer, known_conditions, table) + new : function (integer, string, table) +end +return http_errors diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/net/http/files.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/net/http/files.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,14 @@ +local record serve_options + path : string + mime_map : { string : string } + cache_size : integer + cache_max_file_size : integer + index_files : { string } + directory_index : boolean +end + +local record http_files + serve : function(serve_options|string) : function +end + +return http_files diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/net/http/parser.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/net/http/parser.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,58 @@ +local record httpstream + feed : function(httpstream, string) +end + +local type sink_cb = function () + +local record httppacket + enum http_method + "HEAD" + "GET" + "POST" + "PUT" + "DELETE" + "OPTIONS" + -- etc + end + method : http_method + record url_details + path : string + query : string + end + url : url_details + path : string + enum http_version + "1.0" + "1.1" + end + httpversion : http_version + headers : { string : string } + body : string | boolean + body_sink : sink_cb + chunked : boolean + partial : boolean +end + +local enum error_conditions + "cancelled" + "connection-closed" + "certificate-chain-invalid" + "certificate-verify-failed" + "connection failed" + "invalid-url" + "unable to resolve service" +end + +local type success_cb = function (httppacket) +local type error_cb = function (error_conditions) + +local enum stream_mode + "client" + "server" +end + +local record lib + new : function (success_cb, error_cb, stream_mode) : httpstream +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/net/http/server.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/net/http/server.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,6 @@ + +local record http_server + -- TODO +end + +return http_server diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/net/server.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/net/server.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,65 @@ +local record server + record LuaSocketTCP + end + record LuaSecCTX + end + + record extra_settings + end + + record interface + end + enum socket_type + "tcp" + "tcp6" + "tcp4" + end + + record listeners + onconnect : function (interface) + ondetach : function (interface) + onattach : function (interface, string) + onincoming : function (interface, string, string) + ondrain : function (interface) + onreadtimeout : function (interface) + onstarttls : function (interface) + onstatus : function (interface, string) + ondisconnect : function (interface, string) + end + + get_backend : function () : string + + type port = string | integer + enum read_mode + "*a" + "*l" + end + type read_size = read_mode | integer + addserver : function (string, port, listeners, read_size, LuaSecCTX) : interface + addclient : function (string, port, listeners, read_size, LuaSecCTX, socket_type, extra_settings) : interface + record listen_config + read_size : read_size + tls_ctx : LuaSecCTX + tls_direct : boolean + sni_hosts : { string : LuaSecCTX } + end + listen : function (string, port, listeners, listen_config) : interface + enum quitting + "quitting" + end + loop : function () : quitting + closeall : function () + setquitting : function (boolean | quitting) + + wrapclient : function (LuaSocketTCP, string, port, listeners, read_size, LuaSecCTX, extra_settings) : interface + wrapserver : function (LuaSocketTCP, string, port, listeners, listen_config) : interface + watchfd : function (integer | LuaSocketTCP, function (interface), function (interface)) : interface + link : function () + + record config + end + set_config : function (config) + +end + +return server diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/plugins/mod_cron.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/plugins/mod_cron.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,141 @@ +module:set_global(); + +local async = require "prosody.util.async"; + +local cron_initial_delay = module:get_option_number("cron_initial_delay", 1); +local cron_check_delay = module:get_option_number("cron_check_delay", 3600); +local cron_spread_factor = module:get_option_number("cron_spread_factor", 0); + +local record map_store + -- TODO move to somewhere sensible + get : function (map_store, string, K) : V + set : function (map_store, string, K, V) +end + +local enum frequency + "hourly" + "daily" + "weekly" +end + +local record task_spec + id : string -- unique id + name : string -- name or short description + when : frequency + period : number + last : integer + run : function (task_spec, integer) + save : function (task_spec, integer) + restore : function (task_spec, integer) +end + +local record task_event + source : module + item : task_spec +end + +local active_hosts : { string : boolean } = { } + +function module.add_host(host_module : moduleapi) + + local last_run_times = host_module:open_store("cron", "map") as map_store; + active_hosts[host_module.host] = true; + + local function save_task(task : task_spec, started_at : integer) + last_run_times:set(nil, task.id, started_at); + end + + local function restore_task(task : task_spec) + if task.last == nil then + task.last = last_run_times:get(nil, task.id); + end + end + + local function task_added(event : task_event) : boolean + local task = event.item; + if task.name == nil then + task.name = task.when; + end + if task.id == nil then + task.id = event.source.name .. "/" .. task.name:gsub("%W", "_"):lower(); + end + task.period = host_module:get_option_period(task.id:gsub("/", "_") .. "_period", "1" .. task.when, 60, 86400*7*53); + task.restore = restore_task; + task.save = save_task; + module:log("debug", "%s task %s added", task.when, task.id); + return true; + end + + local function task_removed(event : task_event) : boolean + local task = event.item; + host_module:log("debug", "Task %s removed", task.id); + return true; + end + + host_module:handle_items("task", task_added, task_removed, true); + + function host_module.unload() + active_hosts[host_module.host]=nil; + end +end + +local function should_run(task : task_spec, last : integer) : boolean + return not last or last + task.period * 0.995 <= os.time(); +end + +local function run_task(task : task_spec) + task:restore(); + if not should_run(task, task.last) then + return; + end + local started_at = os.time(); + task:run(started_at); + task.last = started_at; + task:save(started_at); +end + +local function spread(t : number, factor : number) : number + return t * (1 - factor + 2*factor*math.random()); +end + +local task_runner : async.runner_t = async.runner(run_task); +scheduled = module:add_timer(cron_initial_delay, function() : number + module:log("info", "Running periodic tasks"); + local delay = spread(cron_check_delay, cron_spread_factor); + for host in pairs(active_hosts) do + module:log("debug", "Running periodic tasks for host %s", host); + for _, task in ipairs(module:context(host):get_host_items("task") as { task_spec } ) do + task_runner:run(task); + end + end + module:log("debug", "Wait %gs", delay); + return delay; +end); + +-- TODO measure load, pick a good time to do stuff + +module:add_item("shell-command", { + section = "cron"; + section_desc = "View and manage recurring tasks"; + name = "tasks"; + desc = "View registered tasks"; + args = {}; + handler = function (self, filter_host : string) + local format_table = require "prosody.util.human.io".table; + local it = require "util.iterators"; + local row = format_table({ + { title = "Host", width = "2p" }; + { title = "Task", width = "3p" }; + { title = "Desc", width = "3p" }; + { title = "When", width = "1p" }; + { title = "Last run", width = "20" }; + }, self.session.width); + local print = self.session.print; + print(row()); + for host in it.sorted_pairs(filter_host and { [filter_host]=true } or active_hosts) do + for _, task in ipairs(module:context(host):get_host_items("task")) do + print(row { host, task.id, task.name, task.when, task.last and os.date("%Y-%m-%d %R:%S", task.last) or "never" }); + end + end + end; +}); diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/plugins/muc/muc.lib.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/plugins/muc/muc.lib.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,178 @@ +local Stanza = require "prosody.util.stanza".stanza_t + +local record Room + jid : string + + enum Affiliation + "outcast" + "none" + "member" + "admin" + "owner" + end + + enum Role + "none" + "visitor" + "participant" + "moderator" + end + + record Occupant + bare_jid : string + nick : string + sessions : { string : Stanza } + role : Role + jid : string + + choose_new_primary : function (Occupant) : string + set_session : function (Occupant, string, Stanza, boolean) + remove_session : function (Occupant, string) + each_session : function (Occupant) -- TODO Iterator + + end + + -- Private properties + _jid_nick : { string : string } + _occupants : { string : Occupant } + _data : { string : any } + _affiliations : { string : Affiliation } + _affiliation_data : { string : { string : any } } + + -- Occupant methods + get_occupant_jid : function (Room, real_jid : string) : string + new_occupant : function (Room, bare_real_jid : string, nick : string) : Occupant + get_occupant_by_nick : function (Room, nick : string) : Occupant + type OccupantIterator = function ({string:Occupant}, occupant_jid : string) : string, Occupant + each_occupant : function (Room, read_only : boolean) : OccupantIterator, {string:Occupant}, nil + has_occupant : function (Room) : boolean + get_occupant_by_real_jid : function (Room, real_jid : string) : Occupant + save_occupant :function (Room, Occupant) : Occupant + + -- Affiliation methods + type AffiliationIterator = function (any, jid : string) : string, Affiliation + get_affiliation : function (Room, jid : string) : Affiliation + each_affiliation : function (Room, Affiliation) : AffiliationIterator, nil, nil + set_affiliation : function (Room, jid : string, Affiliation, reason : string, data : { string : any }) : boolean, string, string, string -- ok + error tripplet + get_affiliation_data : function (Room, jid : string, key : string) : any + set_affiliation_data : function (Room, jid : string, key : string, value : any) : boolean + get_registered_nick : function (Room, jid : string) : string + get_registered_jid : function (Room, nick : string) : string + + -- Role methods + get_default_role : function (Room, Affiliation) : Role, integer + get_role : function (Room, nick : string) : Role + may_set_role : function (Room, actor : string, Occupant, Role) : boolean + set_role : function (Room, actor : string, occupant_jid : string, Role, reason : string) : boolean, string, string, string + + -- Routing input, generally handled by mod_muc and hooked up to Prosody routing events + handle_first_presence : function (Room, table, Stanza) : boolean + handle_normal_presence : function (Room, table, Stanza) : boolean + handle_presence_to_room : function (Room, table, Stanza) : boolean + handle_presence_to_occupant : function (Room, table, Stanza) : boolean + handle_message_to_room : function (Room, table, Stanza) : boolean + handle_message_to_occupant : function (Room, table, Stanza) : boolean + handle_groupchat_to_room : function (Room, table, Stanza) : boolean + handle_iq_to_occupant : function (Room, table, Stanza) : boolean + handle_disco_info_get_query : function (Room, table, Stanza) : boolean + handle_disco_items_get_query : function (Room, table, Stanza) : boolean + handle_admin_query_set_command : function (Room, table, Stanza) : boolean + handle_admin_query_get_command : function (Room, table, Stanza) : boolean + handle_owner_query_get_to_room : function (Room, table, Stanza) : boolean + handle_owner_query_set_to_room : function (Room, table, Stanza) : boolean + handle_mediated_invite : function (Room, table, Stanza) : boolean + handle_mediated_decline : function (Room, table, Stanza) : boolean + handle_role_request : function (Room, table, Stanza) : boolean + handle_register_iq : function (Room, table, Stanza) : boolean + handle_kickable : function (Room, table, Stanza) : boolean + + -- Routing output + broadcast : function (Room, Stanza, function (nick : string, Occupant) : boolean) + broadcast_message : function (Room, Stanza) : boolean + route_stanza : function (Room, Stanza) + route_to_occupant : function (Room, Occupant, Stanza) + + -- Sending things to someone joining + publicise_occupant_status : function (Room, Occupant, x : Stanza, nick : string, actor : string, reason : string, prev_role : Role, force_unavailable : boolean, recipient : Occupant) + send_occupant_list : function (Room, to : string, filter : function (occupant_jid : string, Occupant) : boolean) + send_history : function (Room, Stanza) + send_subject : function (Room, to : string, time : number) + + respond_to_probe : function (Room, table, Stanza, Occupant) + + -- Constructors for various answer stanzas + get_disco_info : function (Room, Stanza) : Stanza + get_disco_items : function (Room, Stanza) : Stanza + + build_item_list : function (Room, Occupant, Stanza, is_anonymous : boolean, nick : string, actor_nick : string, actor_jid : string, reason : string) : Stanza + build_unavailable_presence : function (Room, from_muc_jid : string, to_jid : string) : Stanza + + -- Form handling + send_form : function (Room, table, Stanza) + get_form_layout : function (Room, actor : string) : table + process_form : function (Room, table, Stanza) : boolean + + -- Properties and configuration + get_name : function (Room) : string + set_name : function (Room, string) : boolean + get_description : function (Room) : string + set_description : function (Room, string) : boolean + get_language : function (Room) : string + set_language : function (Room, string) : boolean + get_hidden : function (Room) : boolean + set_hidden : function (Room, boolean) + get_public : function (Room) : boolean + set_public : function (Room, boolean) + get_password : function (Room) : string + set_password : function (Room, string) : boolean + get_members_only : function (Room) : boolean + set_members_only : function (Room, boolean) : boolean + get_allow_member_invites : function (Room) : boolean + set_allow_member_invites : function (Room, boolean) : boolean + get_moderated : function (Room) : boolean + set_moderated : function (Room, boolean) : boolean + get_persistent : function (Room) : boolean + set_persistent : function (Room, boolean) : boolean + get_changesubject : function (Room) : boolean + set_changesubject : function (Room, boolean) : boolean + get_subject : function (Room) : string + set_subject : function (Room, string) : boolean + get_historylength : function (Room) : integer + set_historylength : function (Room, integer) : boolean + get_presence_broadcast : function (Room) : { Role : boolean } + set_presence_broadcast : function (Room, { Role : boolean }) : boolean + + is_anonymous_for : function (Room, jid : string) : boolean + get_salt : function (Room) : string + get_occupant_id : function (Room, Occupant) + + -- Room teardown + clear : function (Room, x : Stanza) + destroy : function (Room, newjid : string, reason : string, password : string) : boolean + + -- Room state persistence + record FrozenRoom + _jid : string + _data : { string : any } + _affiliation_data : { string : { string : any } } + -- { string : Affiliation } + end + + record StateEntry + bare_jid : string + role : Role + jid : string + end + + save : function (Room, forced : boolean, savestate : boolean) : boolean + freeze : function (Room, live : boolean) : FrozenRoom, { string : StateEntry } +end + +local record lib + new_room : function (jid : string, config : { string : any }) : Room + restore_room : function (Room.FrozenRoom, { string : Room.StateEntry }) : Room + + room_mt : metatable +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/array.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/array.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,9 @@ +local record array_t + { T } +end + +local record lib + metamethod __call : function () : array_t +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/async.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/async.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,42 @@ +local record lib + ready : function () : boolean + waiter : function (num : integer, allow_many : boolean) : function (), function () + guarder : function () : function (id : function ()) : function () | nil + record runner_t + func : function (T) + thread : thread + enum state_e + -- from Lua manual + "running" + "suspended" + "normal" + "dead" + + -- from util.async + "ready" + "error" + end + state : state_e + notified_state : state_e + queue : { T } + type watcher_t = function (runner_t, ... : any) + type watchers_t = { state_e : watcher_t } + data : any + id : string + + run : function (runner_t, T) : boolean, state_e, integer + enqueue : function (runner_t, T) : runner_t + log : function (runner_t, string, string, ... : any) + onready : function (runner_t, function) : runner_t + onready : function (runner_t, function) : runner_t + onwaiting : function (runner_t, function) : runner_t + onerror : function (runner_t, function) : runner_t + end + runner : function (function (T), runner_t.watchers_t, any) : runner_t + wait_for : function (any) : any, any + sleep : function (t:number) + + -- set_nexttick = function(new_next_tick) next_tick = new_next_tick; end; + -- set_schedule_function = function (new_schedule_function) schedule_task = new_schedule_function; end; +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/bitcompat.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/bitcompat.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,8 @@ +local record lib + band : function (integer, integer, ... : integer) : integer + bor : function (integer, integer, ... : integer) : integer + bxor : function (integer, integer, ... : integer) : integer + lshift : function (integer, integer) : integer + rshift : function (integer, integer) : integer +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/compat.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/compat.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,4 @@ +local record lib + xpcall : function (function, function, ...:any):boolean, any +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/crand.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/crand.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,6 @@ +local record lib + bytes : function (n : integer) : string + enum sourceid "OpenSSL" "arc4random()" "Linux" end + _source : sourceid +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/crypto.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/crypto.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,57 @@ +local record lib + record key + private_pem : function (key) : string + public_pem : function (key) : string + get_type : function (key) : string + end + + type base_evp_sign = function (key, message : string) : string + type base_evp_verify = function (key, message : string, signature : string) : boolean + + ed25519_sign : base_evp_sign + ed25519_verify : base_evp_verify + + ecdsa_sha256_sign : base_evp_sign + ecdsa_sha256_verify : base_evp_verify + ecdsa_sha384_sign : base_evp_sign + ecdsa_sha384_verify : base_evp_verify + ecdsa_sha512_sign : base_evp_sign + ecdsa_sha512_verify : base_evp_verify + + rsassa_pkcs1_sha256_sign : base_evp_sign + rsassa_pkcs1_sha256_verify : base_evp_verify + rsassa_pkcs1_sha384_sign : base_evp_sign + rsassa_pkcs1_sha384_verify : base_evp_verify + rsassa_pkcs1_sha512_sign : base_evp_sign + rsassa_pkcs1_sha512_verify : base_evp_verify + + rsassa_pss_sha256_sign : base_evp_sign + rsassa_pss_sha256_verify : base_evp_verify + rsassa_pss_sha384_sign : base_evp_sign + rsassa_pss_sha384_verify : base_evp_verify + rsassa_pss_sha512_sign : base_evp_sign + rsassa_pss_sha512_verify : base_evp_verify + + type Levp_encrypt = function (key : string, iv : string, plaintext : string) : string + type Levp_decrypt = function (key : string, iv : string, ciphertext : string) : string, string + + aes_128_gcm_encrypt : Levp_encrypt + aes_128_gcm_decrypt : Levp_decrypt + aes_256_gcm_encrypt : Levp_encrypt + aes_256_gcm_decrypt : Levp_decrypt + + aes_256_ctr_encrypt : Levp_encrypt + aes_256_ctr_decrypt : Levp_decrypt + + generate_ed25519_keypair : function () : key + + import_private_pem : function (string) : key + import_public_pem : function (string) : key + + parse_ecdsa_signature : function (string, integer) : string, string + build_ecdsa_signature : function (r : string, s : string) : string + + version : string + _LIBCRYPTO_VERSION : string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/dataforms.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/dataforms.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,54 @@ +local stanza_t = require "prosody.util.stanza".stanza_t + +local record lib + record dataform + title : string + instructions : string + + record form_field + + enum field_type + "boolean" + "fixed" + "hidden" + "jid-multi" + "jid-single" + "list-multi" + "list-single" + "text-multi" + "text-private" + "text-single" + end + + type : field_type + var : string -- protocol name + name : string -- internal name + + label : string + desc : string + + datatype : string + range_min : number + range_max : number + + value : any -- depends on field_type + options : table + end + + { form_field } + + enum form_type + "form" + "submit" + "cancel" + "result" + end + + form : function ( dataform, { string : any }, form_type ) : stanza_t + data : function ( dataform, stanza_t ) : { string : any } + end + + new : function ( dataform ) : dataform +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/datamapper.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/datamapper.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,379 @@ +-- Copyright (C) 2021 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- Based on +-- https://json-schema.org/draft/2020-12/json-schema-core.html +-- https://json-schema.org/draft/2020-12/json-schema-validation.html +-- http://spec.openapis.org/oas/v3.0.1#xmlObject +-- https://github.com/OAI/OpenAPI-Specification/issues/630 (text:true) +-- +-- XML Object Extensions: +-- text to refer to the text content at the same time as attributes +-- x_name_is_value for enum fields where the is the value +-- x_single_attribute for +-- +-- TODO pointers +-- TODO cleanup / refactor +-- TODO s/number/integer/ once we have appropriate math.type() compat +-- + +if not math.type then require "prosody.util.mathcompat" end + +local st = require "prosody.util.stanza"; +local json = require "prosody.util.json" +local pointer = require "prosody.util.jsonpointer"; + +local json_type_name = json.json_type_name; +local json_schema_object = require "prosody.util.jsonschema" +local type schema_t = boolean | json_schema_object + +local function toboolean ( s : string ) : boolean + if s == "true" or s == "1" then + return true + elseif s == "false" or s == "0" then + return false + elseif s then + return true + end +end + +local function totype(t : json_type_name, s : string) : any + if not s then return nil end + if t == "string" then + return s; + elseif t == "boolean" then + return toboolean(s) + elseif t == "number" or t == "integer" then + return tonumber(s) + end +end + +local enum value_goes + "in_tag_name" + "in_text" + "in_text_tag" + "in_attribute" + "in_single_attribute" + "in_children" + "in_wrapper" +end + +local function resolve_schema(schema : schema_t, root : json_schema_object) : schema_t + if schema is json_schema_object then + if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then + return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t; + end + end + return schema; +end + +local function guess_schema_type(schema : json_schema_object) : json_type_name + local schema_types = schema.type + if schema_types is json_type_name then + return schema_types + elseif schema_types ~= nil then + error "schema has unsupported 'type' property" + elseif schema.properties then + return "object" + elseif schema.items then + return "array" + end + return "string" -- default assumption +end + +local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string ) + : json_type_name, value_goes, string, string, string, string, { any } + local proptype : json_type_name = "string" + local value_where : value_goes = propname and "in_text_tag" or "in_text" + local name = propname + local namespace : string + local prefix : string + local single_attribute : string + local enums : { any } + + if propschema is json_schema_object then + proptype = guess_schema_type(propschema); + elseif propschema is string then -- Teal says this can never be a string, but it could before so best be sure + error("schema as string is not supported: "..propschema.." {"..current_ns.."}"..propname) + end + + if proptype == "object" or proptype == "array" then + value_where = "in_children" + end + + if propschema is json_schema_object then + local xml = propschema.xml + if xml then + if xml.name then + name = xml.name + end + if xml.namespace and xml.namespace ~= current_ns then + namespace = xml.namespace + end + if xml.prefix then + prefix = xml.prefix + end + if proptype == "array" and xml.wrapped then + value_where = "in_wrapper" + elseif xml.attribute then + value_where = "in_attribute" + elseif xml.text then + value_where = "in_text" + elseif xml.x_name_is_value then + value_where = "in_tag_name" + elseif xml.x_single_attribute then + single_attribute = xml.x_single_attribute + value_where = "in_single_attribute" + end + end + if propschema["const"] then + enums = { propschema["const"] } + elseif propschema["enum"] then + enums = propschema["enum"] + end + end + + return proptype, value_where, name, namespace, prefix, single_attribute, enums +end + +local parse_object : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any } +local parse_array : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { any } + +local function extract_value (s : st.stanza_t, value_where : value_goes, proptype : json.json_type_name, name : string, namespace : string, prefix : string, single_attribute : string, enums : { any }) : string + if value_where == "in_tag_name" then + local c : st.stanza_t + if proptype == "boolean" then + c = s:get_child(name, namespace); + elseif enums and proptype == "string" then + -- XXX O(n²) ? + -- Probably better to flip the table and loop over :childtags(nil, ns), should be 2xO(n) + -- BUT works first, optimize later + for i = 1, #enums do + c = s:get_child(enums[i] as string, namespace); + if c then break end + end + else + c = s:get_child(nil, namespace); + end + if c then + return c.name; + end + elseif value_where == "in_attribute" then + local attr = name + if prefix then + attr = prefix .. ':' .. name + elseif namespace and namespace ~= s.attr.xmlns then + attr = namespace .. "\1" .. name + end + return s.attr[attr] + + elseif value_where == "in_text" then + return s:get_text() + + elseif value_where == "in_single_attribute" then + local c = s:get_child(name, namespace) + return c and c.attr[single_attribute] + elseif value_where == "in_text_tag" then + return s:get_child_text(name, namespace) + end +end + +function parse_object (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any } + local out : { string : any } = {} + schema = resolve_schema(schema, root) + if schema is json_schema_object and schema.properties then + for prop, propschema in pairs(schema.properties) do + propschema = resolve_schema(propschema, root) + + local proptype, value_where, name, namespace, prefix, single_attribute, enums = unpack_propschema(propschema, prop, s.attr.xmlns) + + if value_where == "in_children" and propschema is json_schema_object then + if proptype == "object" then + local c = s:get_child(name, namespace) + if c then + out[prop] = parse_object(propschema, c, root); + end + elseif proptype == "array" then + local a = parse_array(propschema, s, root); + if a and a[1] ~= nil then + out[prop] = a; + end + else + error "unreachable" + end + elseif value_where == "in_wrapper" and propschema is json_schema_object and proptype == "array" then + local wrapper = s:get_child(name, namespace); + if wrapper then + out[prop] = parse_array(propschema, wrapper, root); + end + else + local value : string = extract_value (s, value_where, proptype, name, namespace, prefix, single_attribute, enums) + + out[prop] = totype(proptype, value) + end + end + end + + return out +end + +function parse_array (schema : json_schema_object, s : st.stanza_t, root : json_schema_object) : { any } + local itemschema : schema_t = resolve_schema(schema.items, root); + local proptype, value_where, child_name, namespace, prefix, single_attribute, enums = unpack_propschema(itemschema, nil, s.attr.xmlns) + local attr_name : string + if value_where == "in_single_attribute" then -- FIXME this shouldn't be needed + value_where = "in_attribute"; + attr_name = single_attribute; + end + local out : { any } = {} + + if proptype == "object" then + if itemschema is json_schema_object then + for c in s:childtags(child_name, namespace) do + table.insert(out, parse_object(itemschema, c, root)); + end + else + error "array items must be schema object" + end + elseif proptype == "array" then + if itemschema is json_schema_object then + for c in s:childtags(child_name, namespace) do + table.insert(out, parse_array(itemschema, c, root)); + end + end + else + for c in s:childtags(child_name, namespace) do + local value : string = extract_value (c, value_where, proptype, attr_name or child_name, namespace, prefix, single_attribute, enums) + + table.insert(out, totype(proptype, value)); + end + end + return out; +end + +local function parse (schema : json_schema_object, s : st.stanza_t) : table + local s_type = guess_schema_type(schema) + if s_type == "object" then + return parse_object(schema, s, schema) + elseif s_type == "array" then + return parse_array(schema, s, schema) + else + error "top-level scalars unsupported" + end +end + +local function toxmlstring(proptype : json_type_name, v : any) : string + if proptype == "string" and v is string then + return v + elseif proptype == "number" and v is number then + return string.format("%g", v) + elseif proptype == "integer" and v is number then -- TODO is integer + return string.format("%d", v) + elseif proptype == "boolean" then + return v and "1" or "0" + end +end + +local unparse : function (json_schema_object, table, string, string, st.stanza_t, json_schema_object) : st.stanza_t + +local function unparse_property(out : st.stanza_t, v : any, proptype : json_type_name, propschema : schema_t, value_where : value_goes, name : string, namespace : string, current_ns : string, prefix : string, single_attribute : string, root : json_schema_object) + + if value_where == "in_attribute" then + local attr = name + if prefix then + attr = prefix .. ':' .. name + elseif namespace and namespace ~= current_ns then + attr = namespace .. "\1" .. name + end + + out.attr[attr] = toxmlstring(proptype, v) + elseif value_where == "in_text" then + out:text(toxmlstring(proptype, v)) + elseif value_where == "in_single_attribute" then + assert(single_attribute) + local propattr : { string : string } = {} + + if namespace and namespace ~= current_ns then + propattr.xmlns = namespace + end + + propattr[single_attribute] = toxmlstring(proptype, v) + out:tag(name, propattr):up(); + + else + local propattr : { string : string } + if namespace ~= current_ns then + propattr = { xmlns = namespace } + end + if value_where == "in_tag_name" then + if proptype == "string" and v is string then + out:tag(v, propattr):up(); + elseif proptype == "boolean" and v == true then + out:tag(name, propattr):up(); + end + elseif proptype == "object" and propschema is json_schema_object and v is table then + local c = unparse(propschema, v, name, namespace, nil, root); + if c then + out:add_direct_child(c); + end + elseif proptype == "array" and propschema is json_schema_object and v is table then + if value_where == "in_wrapper" then + local c = unparse(propschema, v, name, namespace, nil, root); + if c then + out:add_direct_child(c); + end + else + unparse(propschema, v, name, namespace, out, root); + end + else + out:text_tag(name, toxmlstring(proptype, v), propattr) + end + end +end + +function unparse ( schema : json_schema_object, t : table, current_name : string, current_ns : string, ctx : st.stanza_t, root : json_schema_object ) : st.stanza_t + + if root == nil then root = schema end + + if schema.xml then + if schema.xml.name then + current_name = schema.xml.name + end + if schema.xml.namespace then + current_ns = schema.xml.namespace + end + -- TODO prefix? + end + + local out = ctx or st.stanza(current_name, { xmlns = current_ns }) + + local s_type = guess_schema_type(schema) + if s_type == "object" then + + for prop, propschema in pairs(schema.properties) do + propschema = resolve_schema(propschema, root) + local v = t[prop] + + if v ~= nil then + local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(propschema, prop, current_ns) + unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) + end + end + return out; + + elseif s_type == "array" then + local itemschema = resolve_schema(schema.items, root) + local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns) + for _, item in ipairs(t as { string }) do + unparse_property(out, item, proptype, itemschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) + end + return out; + end +end + +return { + parse = parse, + unparse = unparse, +} diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/datetime.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/datetime.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,9 @@ +local record lib + date : function (t : number) : string + datetime : function (t : number) : string + time : function (t : number) : string + legacy : function (t : number) : string + parse : function (t : string) : number +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/encodings.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/encodings.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,27 @@ +-- TODO many actually return Maybe(String) +local record lib + record base64 + encode : function (s : string) : string + decode : function (s : string) : string + end + record stringprep + nameprep : function (s : string, strict : boolean) : string + nodeprep : function (s : string, strict : boolean) : string + resourceprep : function (s : string, strict : boolean) : string + saslprep : function (s : string, strict : boolean) : string + end + record idna + to_ascii : function (s : string) : string + to_unicode : function (s : string) : string + end + record utf8 + valid : function (s : string) : boolean + length : function (s : string) : integer + end + record confusable + skeleton : function (s : string) : string + end + version : string +end +return lib + diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/error.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/error.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,78 @@ +local enum error_type + "auth" + "cancel" + "continue" + "modify" + "wait" +end + +local enum error_condition + "bad-request" + "conflict" + "feature-not-implemented" + "forbidden" + "gone" + "internal-server-error" + "item-not-found" + "jid-malformed" + "not-acceptable" + "not-allowed" + "not-authorized" + "policy-violation" + "recipient-unavailable" + "redirect" + "registration-required" + "remote-server-not-found" + "remote-server-timeout" + "resource-constraint" + "service-unavailable" + "subscription-required" + "undefined-condition" + "unexpected-request" +end + +local record protoerror + type : error_type + condition : error_condition + text : string + code : integer +end + +local record Error + type : error_type + condition : error_condition + text : string + code : integer + context : { any : any } + source : string +end + +local type compact_registry_item = { string, string, string, string } +local type compact_registry = { compact_registry_item } +local type registry = { string : protoerror } +local type context = { string : any } + +local record error_registry_wrapper + source : string + registry : registry + new : function (string, context) : Error + coerce : function (any, string) : any, Error + wrap : function (Error) : Error + wrap : function (string, context) : Error + is_error : function (any) : boolean +end + +local record lib + record configure_opt + auto_inject_traceback : boolean + end + new : function (protoerror, context, { string : protoerror }, string) : Error + init : function (string, string, registry | compact_registry) : error_registry_wrapper + init : function (string, registry | compact_registry) : error_registry_wrapper + is_error : function (any) : boolean + coerce : function (any, string) : any, Error + from_stanza : function (table, context, string) : Error + configure : function +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/format.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/format.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,4 @@ +local record lib + format : function (string, ... : any) : string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/hashes.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/hashes.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,35 @@ +local type hash = function (msg : string, hex : boolean) : string +local type hmac = function (key : string, msg : string, hex : boolean) : string +local type kdf = function (pass : string, salt : string, i : integer) : string + +local record lib + sha1 : hash + sha224 : hash + sha256 : hash + sha384 : hash + sha512 : hash + md5 : hash + sha3_256 : hash + sha3_512 : hash + blake2s256 : hash + blake2b512 : hash + hmac_sha1 : hmac + hmac_sha224 : hmac + hmac_sha256 : hmac + hmac_sha384 :hmac + hmac_sha512 : hmac + hmac_md5 : hmac + hmac_sha3_256 : hmac + hmac_sha3_512 : hmac + hmac_blake2s256 : hmac + hmac_blake2b512 : hmac + scram_Hi_sha1 : kdf + pbkdf2_hmac_sha1 : kdf + pbkdf2_hmac_sha256 : kdf + hkdf_hmac_sha256 : kdf + hkdf_hmac_sha384 : kdf + equals : function (string, string) : boolean + version : string + _LIBCRYPTO_VERSION : string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/hex.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/hex.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,8 @@ +local type s2s = function (s : string) : string +local record lib + to : s2s + from : s2s + encode : s2s + decode : s2s +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/http.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/http.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,9 @@ +local record lib + urlencode : function (s : string) : string + urldecode : function (s : string) : string + formencode : function (f : { string : string }) : string + formdecode : function (s : string) : { string : string } + contains_token : function (field : string, token : string) : boolean + normalize_path : function (path : string) : string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/human/io.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/human/io.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,28 @@ +local record lib + getchar : function (n : integer) : string + getline : function () : string + getpass : function () : string + show_yesno : function (prompt : string) : boolean + read_password : function () : string + show_prompt : function (prompt : string) : boolean + printf : function (fmt : string, ... : any) + padleft : function (s : string, width : integer) : string + padright : function (s : string, width : integer) : string + + -- {K:V} vs T ? + record tablerow + width : integer | string -- generate an 1..100 % enum? + title : string + mapper : function (V, {K:V}) : string + key : K + enum alignments + "left" + "right" + end + align : alignments + end + type getrow = function ({ K : V }) : string + table : function ({ tablerow }, width : integer) : getrow +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/human/units.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/human/units.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,8 @@ +local lib = record + enum logbase + "b" -- 1024 + end + adjust : function (number, string) : number, string + format : function (number, string, logbase) : string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/id.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/id.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,9 @@ +local record lib + short : function () : string + medium : function () : string + long : function () : string + custom : function (integer) : function () : string + +end +return lib + diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/interpolation.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/interpolation.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,6 @@ +local type renderer = function (string, { string : any }) : string +local type filter = function (string, any) : string +local record lib + new : function (string, string, funcs : { string : filter }) : renderer +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/ip.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/ip.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,20 @@ +local record iplib + enum protocol + "IPv6" + "IPv4" + end + record ip_t + addr : string + packed : string + proto : protocol + zone : string + end + + new_ip : function (string, protocol) : ip_t + commonPrefixLength : function (ip_t, ip_t) : integer + parse_cidr : function (string) : ip_t, integer + match : function (ip_t, ip_t, integer) : boolean + is_ip : function (any) : boolean + truncate : function (ip_t, integer) : ip_t +end +return iplib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/jid.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/jid.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,15 @@ +local record lib + split : function (string) : string, string, string + bare : function (string) : string + prepped_split : function (string, boolean) : string, string, string + join : function (string, string, string) : string + prep : function (string, boolean) : string + compare : function (string, string) : boolean + node : function (string) : string + host : function (string) : string + resource : function (string) : string + escape : function (string) : string + unescape : function (string) : string +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/json.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/json.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,18 @@ +local record lib + encode : function (any) : string + decode : function (string) : any, string + + enum json_type_name + "null" + "boolean" + "object" + "array" + "number" + "string" + "integer" + end + + type null_type = (nil) + null : null_type +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/jsonpointer.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/jsonpointer.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,46 @@ + +local enum ptr_error + "invalid-table" + "invalid-path" +end + +local function unescape_token(escaped_token : string) : string + local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~") + return unescaped +end + +local function resolve_json_pointer(ref : any, path : string) : any, ptr_error + local ptr_len = #path+1 + for part, pos in path:gmatch("/([^/]*)()") do + local token = unescape_token(part) + if not ref is table then + return nil + end + local idx = next(ref) + local new_ref : any + + if idx is string then + new_ref = ref[token] + elseif idx is integer then + local i = tonumber(token) + if token == "-" then i = #(ref as {any}) + 1 end + new_ref = ref[i+1] + else + return nil, "invalid-table" + end + + if pos as integer == ptr_len then + return new_ref + elseif new_ref is table then + ref = new_ref + elseif not ref is table then + return nil, "invalid-path" + end + + end + return ref +end + +return { + resolve = resolve_json_pointer, +} diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/jsonschema.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/jsonschema.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,505 @@ +-- Copyright (C) Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- Based on +-- https://json-schema.org/draft/2020-12/json-schema-core.html +-- https://json-schema.org/draft/2020-12/json-schema-validation.html +-- + +if not math.type then require "prosody.util.mathcompat" end + + +local utf8_enc = rawget(_G, "utf8") or require"prosody.util.encodings".utf8; +local utf8_len = utf8_enc.len or function(s : string) : integer + local _, count = s:gsub("[%z\001-\127\194-\253][\128-\191]*", ""); + return count; +end; + +local json = require "prosody.util.json" +local null = json.null; + +local pointer = require "prosody.util.jsonpointer" + +local type json_type_name = json.json_type_name + +-- json_type_name here is non-standard +local type schema_t = boolean | json_schema_object + +local record json_schema_object + type json_type_name = json.json_type_name + type schema_object = json_schema_object + + -- json-schema-core meta stuff + ["$schema"] : string + ["$vocabulary"] : { string : boolean } + ["$id"] : string + ["$comment"] : string + ["$defs"] : { string : schema_t } + ["$anchor"] : string -- NYI + ["$dynamicAnchor"] : string -- NYI + ["$ref"] : string + ["$dynamicRef"] : string -- NYI + + -- combinations + allOf : { schema_t } + anyOf : { schema_t } + oneOf : { schema_t } + + -- conditional logic + ["not"] : schema_t + ["if"] : schema_t + ["then"] : schema_t + ["else"] : schema_t + + dependentRequired : { string : { string } } + + -- arrays + prefixItems : { schema_t } + items : schema_t + contains : schema_t + + -- objects + properties : { string : schema_t } + patternProperties: { string : schema_t } -- NYI + additionalProperties: schema_t + propertyNames : schema_t + + -- unevaluated + unevaluatedItems : schema_t -- NYI + unevaluatedProperties : schema_t -- NYI + + -- json-schema-validation + type : json_type_name | { json_type_name } + enum : { any } + const : any + + -- numbers + multipleOf : number + maximum : number + exclusiveMaximum : number + minimum : number + exclusiveMinimum : number + + -- strings + maxLength : integer + minLength : integer + pattern : string -- NYI + + -- arrays + maxItems : integer + minItems : integer + uniqueItems : boolean + maxContains : integer + minContains : integer + + -- objects + maxProperties : integer -- NYI + minProperties : integer -- NYI + required : { string } + dependentSchemas : { string : schema_t } + + -- semantic format + format : string + + -- for Lua + luaPatternProperties: { string : schema_t } + luaPattern : string + + -- xml + record xml_t + name : string + namespace : string + prefix : string + attribute : boolean + wrapped : boolean + + -- nonstantard, maybe in the future + text : boolean + x_name_is_value : boolean + x_single_attribute : string + end + + xml : xml_t + + -- descriptive + title : string + description : string + deprecated : boolean + readOnly : boolean + writeOnly : boolean + + -- methods + validate : function (schema : schema_t, data : any, root : json_schema_object, sloc : string, iloc : string, errs:errors) : boolean, errors +end + +-- TODO validator function per schema property + +local function simple_validate(schema : json_type_name | { json_type_name }, data : any) : boolean + if schema == nil then + return true + elseif schema == "object" and data is table then + return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "string") + elseif schema == "array" and data is table then + return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "number") + elseif schema == "integer" then + return math.type(data) == schema + elseif schema == "null" then + return data == null + elseif schema is { json_type_name } then + for _, one in ipairs(schema as { json_type_name }) do + if simple_validate(one, data) then + return true + end + end + return false + else + return type(data) == schema + end +end + +local record validation_error + instanceLocation : string + schemaLocation : string + error : string +end +local type errors = { validation_error } +local function mkerr(sloc:string,iloc:string,err:string) : validation_error + return { schemaLocation = sloc; instanceLocation = iloc; error = err }; +end + +local function validate (schema : schema_t, data : any, root : json_schema_object, sloc : string, iloc : string, errs:errors) : boolean, errors + if schema is boolean then + return schema + end + + if root == nil then + root = schema as json_schema_object + iloc = "" + sloc = "" + errs = {}; + end + + if schema["$ref"] and schema["$ref"]:sub(1,1) == "#" then + local referenced = pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t + if referenced ~= nil and referenced ~= root and referenced ~= schema then + if not validate(referenced, data, root, schema["$ref"], iloc, errs) then + table.insert(errs, mkerr(sloc.."/$ref", iloc, "Subschema failed validation")) + return false, errs; + end + end + end + + if not simple_validate(schema.type, data) then + table.insert(errs, mkerr(sloc.."/type", iloc, "unexpected type")); + return false, errs; + end + + if schema.type == "object" then + if data is table then + -- just check that there the keys are all strings + for k in pairs(data) do + if not k is string then + table.insert(errs, mkerr(sloc.."/type", iloc, "'object' had non-string keys")); + return false, errs; + end + end + end + end + + if schema.type == "array" then + if data is table then + -- just check that there the keys are all numbers + for i in pairs(data) do + if not i is integer then + table.insert(errs, mkerr(sloc.."/type", iloc, "'array' had non-integer keys")); + return false, errs; + end + end + end + end + + if schema["enum"] ~= nil then + local match = false + for _, v in ipairs(schema["enum"]) do + if v == data then + -- FIXME supposed to do deep-compare + match = true + break + end + end + if not match then + table.insert(errs, mkerr(sloc.."/enum", iloc, "not one of the enumerated values")); + return false, errs; + end + end + + -- XXX this is measured in byte, while JSON measures in ... bork + -- TODO use utf8.len? + if data is string then + if schema.maxLength and utf8_len(data) > schema.maxLength then + table.insert(errs, mkerr(sloc.."/maxLength", iloc, "string too long")) + return false, errs; + end + if schema.minLength and utf8_len(data) < schema.minLength then + table.insert(errs, mkerr(sloc.."/maxLength", iloc, "string too short")) + return false, errs; + end + if schema.luaPattern and not data:match(schema.luaPattern) then + table.insert(errs, mkerr(sloc.."/luaPattern", iloc, "string does not match pattern")) + return false, errs; + end + end + + if data is number then + if schema.multipleOf and (data == 0 or data % schema.multipleOf ~= 0) then + table.insert(errs, mkerr(sloc.."/luaPattern", iloc, "not a multiple")) + return false, errs; + end + + if schema.maximum and not ( data <= schema.maximum ) then + table.insert(errs, mkerr(sloc.."/maximum", iloc, "number exceeds maximum")) + return false, errs; + end + + if schema.exclusiveMaximum and not ( data < schema.exclusiveMaximum ) then + table.insert(errs, mkerr(sloc.."/exclusiveMaximum", iloc, "number exceeds exclusive maximum")) + return false, errs; + end + + if schema.minimum and not ( data >= schema.minimum ) then + table.insert(errs, mkerr(sloc.."/minimum", iloc, "number below minimum")) + return false, errs; + end + + if schema.exclusiveMinimum and not ( data > schema.exclusiveMinimum ) then + table.insert(errs, mkerr(sloc.."/exclusiveMinimum", iloc, "number below exclusive minimum")) + return false, errs; + end + end + + if schema.allOf then + for i, sub in ipairs(schema.allOf) do + if not validate(sub, data, root, sloc.."/allOf/"..i, iloc, errs) then + table.insert(errs, mkerr(sloc.."/allOf", iloc, "did not match all subschemas")) + return false, errs; + end + end + end + + if schema.oneOf then + local valid = 0 + for i, sub in ipairs(schema.oneOf) do + if validate(sub, data, root, sloc.."/oneOf"..i, iloc, errs) then + valid = valid + 1 + end + end + if valid ~= 1 then + table.insert(errs, mkerr(sloc.."/oneOf", iloc, "did not match exactly one subschema")) + return false, errs; + end + end + + if schema.anyOf then + local match = false + for i, sub in ipairs(schema.anyOf) do + if validate(sub, data, root, sloc.."/anyOf/"..i, iloc, errs) then + match = true + break + end + end + if not match then + table.insert(errs, mkerr(sloc.."/anyOf", iloc, "did not match any subschema")) + return false, errs; + end + end + + if schema["not"] then + if validate(schema["not"], data, root, sloc.."/not", iloc, errs) then + table.insert(errs, mkerr(sloc.."/not", iloc, "did match subschema")) + return false, errs; + end + end + + if schema["if"] ~= nil then + if validate(schema["if"], data, root, sloc.."/if", iloc, errs) then + if schema["then"] then + if not validate(schema["then"], data, root, sloc.."/then", iloc, errs) then + table.insert(errs, mkerr(sloc.."/then", iloc, "did not match subschema")) + return false, errs; + end + end + else + if schema["else"] then + if not validate(schema["else"], data, root, sloc.."/else", iloc, errs) then + table.insert(errs, mkerr(sloc.."/else", iloc, "did not match subschema")) + return false, errs; + end + end + end + end + + if schema.const ~= nil and schema.const ~= data then + table.insert(errs, mkerr(sloc.."/const", iloc, "did not match constant value")) + return false, errs; + end + + if data is table then + -- tables combine object and array behavior, thus we do both kinds of + -- validations in this block, which could be useful for validating Lua + -- tables + + if schema.maxItems and #(data as {any}) > schema.maxItems then + table.insert(errs, mkerr(sloc.."/maxItems", iloc, "too many items")) + return false, errs; + end + + if schema.minItems and #(data as {any}) < schema.minItems then + table.insert(errs, mkerr(sloc.."/minItems", iloc, "too few items")) + return false, errs; + end + + if schema.required then + for _, k in ipairs(schema.required) do + if data[k] == nil then + table.insert(errs, mkerr(sloc.."/required", iloc.."/"..tostring(k), "missing required property")) + return false, errs; + end + end + end + + if schema.dependentRequired then + for k, reqs in pairs(schema.dependentRequired) do + if data[k] ~= nil then + for _, req in ipairs(reqs) do + if data[req] == nil then + table.insert(errs, mkerr(sloc.."/dependentRequired", iloc, "missing dependent required property")) + return false, errs; + end + end + end + end + end + + if schema.propertyNames ~= nil then + -- could be used to validate non-string keys of Lua tables + for k in pairs(data) do + if not validate(schema.propertyNames, k, root, sloc.."/propertyNames", iloc.."/"..tostring(k), errs) then + table.insert(errs, mkerr(sloc.."/propertyNames", iloc.."/"..tostring(k), "a property name did not match subschema")) + return false, errs; + end + end + end + + -- additionalProperties applies to properties not validated by properties + -- or patternProperties, so we must keep track of properties validated by + -- the later + local seen_properties : { string : boolean } = {} + + if schema.properties then + for k, sub in pairs(schema.properties) do + if data[k] ~= nil and not validate(sub, data[k], root, sloc.."/"..tostring(k), iloc.."/"..tostring(k), errs) then + table.insert(errs, mkerr(sloc.."/"..tostring(k), iloc.."/"..tostring(k), "a property did not match subschema")) + return false, errs; + end + seen_properties[k] = true + end + end + + if schema.luaPatternProperties then + -- like patternProperties, but Lua patterns + for pattern, sub in pairs(schema.luaPatternProperties) do + for k in pairs(data) do + if k is string and k:match(pattern) then + if not validate(sub, data[k], root, sloc.."/luaPatternProperties", iloc, errs) then + table.insert(errs, mkerr(sloc.."/luaPatternProperties/"..pattern, iloc.."/"..tostring(k), "a property did not match subschema")) + return false, errs; + end + seen_properties[k] = true + end + end + end + end + + if schema.additionalProperties ~= nil then + for k, v in pairs(data) do + if not seen_properties[k as string] then + if not validate(schema.additionalProperties, v, root, sloc.."/additionalProperties", iloc.."/"..tostring(k), errs) then + table.insert(errs, mkerr(sloc.."/additionalProperties", iloc.."/"..tostring(k), "additional property did not match subschema")) + return false, errs; + end + end + end + end + + if schema.dependentSchemas then + for k, sub in pairs(schema.dependentSchemas) do + if data[k] ~= nil and not validate(sub, data, root, sloc.."/dependentSchemas/"..k, iloc, errs) then + table.insert(errs, mkerr(sloc.."/dependentSchemas", iloc.."/"..tostring(k), "did not match dependent subschema")) + return false, errs; + end + end + end + + if schema.uniqueItems then + -- only works for scalars, would need to deep-compare for objects/arrays/tables + local values : { any : boolean } = {} + for _, v in pairs(data) do + if values[v] then + table.insert(errs, mkerr(sloc.."/uniqueItems", iloc, "had duplicate items")) + return false, errs; + end + values[v] = true + end + end + + local p = 0 + if schema.prefixItems ~= nil then + for i, s in ipairs(schema.prefixItems) do + if data[i] == nil then + break + elseif validate(s, data[i], root, sloc.."/prefixItems/"..i, iloc.."/"..i, errs) then + p = i + else + table.insert(errs, mkerr(sloc.."/prefixItems/"..i, iloc.."/"..tostring(i), "did not match subschema")) + return false, errs; + end + end + end + + if schema.items ~= nil then + for i = p+1, #(data as {any}) do + if not validate(schema.items, data[i], root, sloc, iloc.."/"..i, errs) then + table.insert(errs, mkerr(sloc.."/prefixItems/"..i, iloc.."/"..i, "did not match subschema")) + return false, errs; + end + end + end + + if schema.contains ~= nil then + local found = 0 + for i = 1, #(data as {any}) do + if validate(schema.contains, data[i], root, sloc.."/contains", iloc.."/"..i, errs) then + found = found + 1 + else + table.insert(errs, mkerr(sloc.."/contains", iloc.."/"..i, "did not match subschema")) + end + end + if found < (schema.minContains or 1) then + table.insert(errs, mkerr(sloc.."/minContains", iloc, "too few matches")) + return false, errs; + elseif found > (schema.maxContains or math.huge) then + table.insert(errs, mkerr(sloc.."/maxContains", iloc, "too many matches")) + return false, errs; + end + end + end + + return true; +end + + +json_schema_object.validate = validate; + +return json_schema_object; diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/jwt.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/jwt.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,38 @@ +local crypto = require "prosody.util.crypto" +local record jwtlib + enum algorithm + "HS256" + "HS384" + "HS512" + "ES256" + "ES512" + "RS256" + "RS384" + "RS512" + "PS256" + "PS384" + "PS512" + end + type payload = { string : any } + type signer_t = function (payload : payload) : string + type verifier_t = function (token : string) : payload + enum key_type + "rsaEncryption" + "id-ecPublicKey" + end + record algorithm_t + sign : signer_t + verify : verifier_t + load_key : function (key : string) : crypto.key + end + init : function (algorithm, private_key : string, public_key : string, table) : signer_t, verifier_t + new_signer : function (algorithm, string, table) : signer_t + new_verifier : function (algorithm, string, table) : verifier_t + _algorithms : { + algorithm : algorithm_t + } + -- Deprecated + sign : function (private_key : string, payload) : string + verify : function (string) : payload +end +return jwtlib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/logger.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/logger.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,18 @@ +local record util + enum loglevel + "debug" + "info" + "warn" + "error" + end + type logger = function ( loglevel, string, ...:any ) + type sink = function ( string, loglevel, string, ...:any ) + type simple_sink = function ( string, loglevel, string ) + init : function ( string ) : logger + make_logger : function ( string, loglevel ) : function ( string, ...:any ) + reset : function () + add_level_sink : function ( loglevel, sink ) + add_simple_sink : function ( simple_sink, { loglevel } ) +end + +return util diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/mathcompat.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/mathcompat.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,15 @@ +if not math.type then + local enum number_subtype + "float" "integer" + end + local function math_type(t:any) : number_subtype + if t is number then + if t % 1 == 0 and t ~= t+1 and t ~= t-1 then + return "integer" + else + return "float" + end + end + end + _G.math.type = math_type +end diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/net.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/net.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,13 @@ + +local enum type_strings + "both" + "ipv4" + "ipv6" +end + +local record lib + local_addresses : function (type_strings, boolean) : { string } + pton : function (string):string + ntop : function (string):string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/poll.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/poll.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,31 @@ +local record state + enum waiterr + "timeout" + "signal" + end + add : function (state, integer, boolean, boolean) : boolean + add : function (state, integer, boolean, boolean) : nil, string, integer + set : function (state, integer, boolean, boolean) : boolean + set : function (state, integer, boolean, boolean) : nil, string, integer + del : function (state, integer) : boolean + del : function (state, integer) : nil, string, integer + wait : function (state, integer) : integer, boolean, boolean + wait : function (state, integer) : nil, string, integer + wait : function (state, integer) : nil, waiterr + getfd : function (state) : integer +end + +local record lib + new : function () : state + EEXIST : integer + EMFILE : integer + ENOENT : integer + enum api_backend + "epoll" + "poll" + "select" + end + api : api_backend +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/pposix.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/pposix.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,109 @@ +local record pposix + enum syslog_facility + "auth" + "authpriv" + "cron" + "daemon" + "ftp" + "kern" + "local0" + "local1" + "local2" + "local3" + "local4" + "local5" + "local6" + "local7" + "lpr" + "mail" + "syslog" + "user" + "uucp" + end + + enum syslog_level + "debug" + "info" + "notice" + "warn" + "error" + end + + enum ulimit_resource + "CORE" + "CPU" + "DATA" + "FSIZE" + "NOFILE" + "STACK" + "MEMLOCK" + "NPROC" + "RSS" + "NICE" + end + + enum ulimit_unlimited + "unlimited" + end + + type ulimit_limit = integer | ulimit_unlimited + + record utsname + sysname : string + nodename : string + release : string + version : string + machine : string + domainname : string + end + + record memoryinfo + allocated : integer + allocated_mmap : integer + used : integer + unused : integer + returnable : integer + end + + abort : function () + + daemonize : function () : boolean, string + + syslog_open : function (ident : string, facility : syslog_facility) + syslog_close : function () + syslog_log : function (level : syslog_level, src : string, msg : string) + syslog_setminlevel : function (level : syslog_level) + + getpid : function () : integer + getuid : function () : integer + getgid : function () : integer + + setuid : function (uid : integer | string) : boolean, string -- string|integer + setgid : function (uid : integer | string) : boolean, string + initgroups : function (user : string, gid : integer) : boolean, string + + umask : function (umask : string) : string + + mkdir : function (dir : string) : boolean, string + + setrlimit : function (resource : ulimit_resource, soft : ulimit_limit, hard : ulimit_limit) : boolean, string + getrlimit : function (resource : ulimit_resource) : boolean, ulimit_limit, ulimit_limit + getrlimit : function (resource : ulimit_resource) : boolean, string + + uname : function () : utsname + + setenv : function (key : string, value : string) : boolean + + meminfo : function () : memoryinfo + + atomic_append : function (f : FILE, s : string) : boolean, string, integer + remove_blocks : function (f : FILE, integer, integer) + + isatty : function(FILE) : boolean + + ENOENT : integer + _NAME : string + _VESRION : string +end + +return pposix diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/promise.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/promise.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,22 @@ + +local record lib + type resolve_func = function (any) + type promise_body = function (resolve_func, resolve_func) + + record Promise + type on_resolved = function (A) : any + type on_rejected = function (B) : any + next : function (Promise, on_resolved, on_rejected) : Promise + end + + new : function (promise_body) : Promise + resolve : function (any) : Promise + reject : function (any) : Promise + all : function ({ Promise }) : Promise + all_settled : function ({ Promise }) : Promise + race : function ({ Promise }) : Promise + try : function + is_promise : function(any) : boolean +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/queue.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/queue.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,21 @@ +local record lib + record queue + size : integer + count : function (queue) : integer + enum push_errors + "queue full" + end + + push : function (queue, T) : boolean, push_errors + pop : function (queue) : T + peek : function (queue) : T + replace : function (queue, T) : boolean, push_errors + type iterator = function (T, integer) : integer, T + items : function (queue) : iterator, T, integer + type consume_iter = function (queue) : T + consume : function (queue) : consume_iter + end + + new : function (size:integer, allow_wrapping:boolean) : queue +end +return lib; diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/random.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/random.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,4 @@ +local record lib + bytes : function (n:integer):string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/ringbuffer.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/ringbuffer.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,20 @@ +local record lib + record ringbuffer + find : function (ringbuffer, string) : integer + discard : function (ringbuffer, integer) : boolean + read : function (ringbuffer, integer, boolean) : string + readuntil : function (ringbuffer, string) : string + write : function (ringbuffer, string) : integer + size : function (ringbuffer) : integer + length : function (ringbuffer) : integer + sub : function (ringbuffer, integer, integer) : string + byte : function (ringbuffer, integer, integer) : integer... + free : function (ringbuffer) : integer + end + + new : function (integer) : ringbuffer +end + +return lib + + diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/roles.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/roles.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,32 @@ +local record util_roles + + type context = any + + record Role + id : string + name : string + description : string + default : boolean + priority : number -- or integer? + permissions : { string : boolean } + + may : function (Role, string, context) + clone : function (Role, role_config) + set_permission : function (Role, string, boolean, boolean) + end + + is_role : function (any) : boolean + + record role_config + name : string + description : string + default : boolean + priority : number -- or integer? + inherits : { Role } + permissions : { string : boolean } + end + + new : function (role_config, Role) : Role +end + +return util_roles diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/serialization.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/serialization.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,34 @@ +local record _M + enum preset + "debug" + "oneline" + "compact" + "pretty" + end + type fallback = function (any, string) : string + record config + preset : preset + fallback : fallback + fatal : boolean + keywords : { string : boolean } + indentwith : string + itemstart : string + itemsep : string + itemlast : string + tstart : string + tend : string + kstart : string + kend : string + equals : string + unquoted : boolean | string + hex : string + freeze : boolean + maxdepth : integer + multirefs : boolean + table_pairs : function + end + type serializer = function (any) : string + new : function (config|preset) : serializer + serialize : function (any, config|preset) : string +end +return _M diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/set.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/set.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,22 @@ +local record lib + record Set + add : function (Set, T) + contains : function (Set, T) : boolean + contains_set : function (Set, Set) : boolean + items : function (Set) : function (Set, T) : T + remove : function (Set, T) + add_list : function (Set, { T }) + include : function (Set, Set) + exclude : function (Set, Set) + empty : function (Set) : boolean + end + + new : function ({ T }) : Set + is_set : function (any) : boolean + union : function (Set, Set) : Set + difference : function (Set, Set) : Set + intersection : function (Set, Set) : Set + xor : function (Set, Set) : Set +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/signal.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/signal.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,41 @@ +local record lib + enum Signal + "SIGABRT" + "SIGALRM" + "SIGBUS" + "SIGCHLD" + "SIGCLD" + "SIGCONT" + "SIGFPE" + "SIGHUP" + "SIGILL" + "SIGINT" + "SIGIO" + "SIGIOT" + "SIGKILL" + "SIGPIPE" + "SIGPOLL" + "SIGPROF" + "SIGQUIT" + "SIGSEGV" + "SIGSTKFLT" + "SIGSTOP" + "SIGSYS" + "SIGTERM" + "SIGTRAP" + "SIGTTIN" + "SIGTTOU" + "SIGURG" + "SIGUSR1" + "SIGUSR2" + "SIGVTALRM" + "SIGWINCH" + "SIGXCPU" + "SIGXFSZ" + end + signal : function (integer | Signal, function, boolean) : boolean + raise : function (integer | Signal) + kill : function (integer, integer | Signal) + -- enum : integer +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/smqueue.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/smqueue.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,99 @@ +local queue = require "prosody.util.queue"; + +local record lib + -- T would typically be util.stanza + record smqueue + _queue : queue.queue + _head : integer + _tail : integer + + enum ack_errors + "tail" + "head" + "pop" + end + push : function (smqueue, T) + ack : function (smqueue, integer) : { T }, ack_errors + resumable : function (smqueue) : boolean + resume : function (smqueue) : queue.queue.iterator, any, integer + type consume_iter = function (smqueue) : T + consume : function (smqueue) : consume_iter + + table : function (smqueue) : { T } + end + new : function (integer) : smqueue +end + +local type smqueue = lib.smqueue; + +function smqueue:push(v) + self._head = self._head + 1; + -- Wraps instead of errors + assert(self._queue:push(v)); +end + +function smqueue:ack(h : integer) : { any }, smqueue.ack_errors + if h < self._tail then + return nil, "tail"; + elseif h > self._head then + return nil, "head"; + end + -- TODO optimize? cache table fields + local acked = {}; + self._tail = h; + local expect = self._head - self._tail; + while expect < self._queue:count() do + local v = self._queue:pop(); + if not v then return nil, "pop"; end + table.insert(acked, v); + end + return acked; +end + +function smqueue:count_unacked() : integer + return self._head - self._tail; +end + +function smqueue:count_acked() : integer + return self._tail; +end + +function smqueue:resumable() : boolean + return self._queue:count() >= (self._head - self._tail); +end + +function smqueue:resume() : queue.queue.iterator, any, integer + return self._queue:items(); +end + +function smqueue:consume() : queue.queue.consume_iter + return self._queue:consume() +end + +-- Compatibility layer, plain ol' table +function smqueue:table() : { any } + local t : { any } = {}; + for i, v in self:resume() do + t[i] = v; + end + return t; +end + +local function freeze(q : smqueue) : { string:integer } + return { head = q._head, tail = q._tail } +end + +local queue_mt = { + -- + __name = "smqueue"; + __index = smqueue; + __len = smqueue.count_unacked; + __freeze = freeze; +} + +function lib.new(size : integer) : queue.queue + assert(size>0); + return setmetatable({ _head = 0; _tail = 0; _queue = queue.new(size, true) }, queue_mt); +end + +return lib; diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/stanza.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/stanza.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,145 @@ +local record lib + + type children_iter = function ( stanza_t ) : stanza_t + type childtags_iter = function () : stanza_t + type maptags_cb = function ( stanza_t ) : stanza_t + + + enum stanza_error_type + "auth" + "cancel" + "continue" + "modify" + "wait" + end + enum stanza_error_condition + "bad-request" + "conflict" + "feature-not-implemented" + "forbidden" + "gone" + "internal-server-error" + "item-not-found" + "jid-malformed" + "not-acceptable" + "not-allowed" + "not-authorized" + "policy-violation" + "recipient-unavailable" + "redirect" + "registration-required" + "remote-server-not-found" + "remote-server-timeout" + "resource-constraint" + "service-unavailable" + "subscription-required" + "undefined-condition" + "unexpected-request" + end + + record stanza_t + name : string + attr : { string : string } + { stanza_t | string } + tags : { stanza_t } + + query : function ( stanza_t, string ) : stanza_t + body : function ( stanza_t, string, { string : string } ) : stanza_t + text_tag : function ( stanza_t, string, string, { string : string } ) : stanza_t + tag : function ( stanza_t, string, { string : string } ) : stanza_t + text : function ( stanza_t, string ) : stanza_t + up : function ( stanza_t ) : stanza_t + at_top : function ( stanza_t ) : boolean + reset : function ( stanza_t ) : stanza_t + add_direct_child : function ( stanza_t, stanza_t ) + add_child : function ( stanza_t, stanza_t ) + remove_children : function ( stanza_t, string, string ) : stanza_t + + get_child : function ( stanza_t, string, string ) : stanza_t + get_text : function ( stanza_t ) : string + get_child_text : function ( stanza_t, string, string ) : string + get_child_attr : function ( stanza_t, string, string ) : string + get_child_with_attr : function ( stanza_t, string, string, string, function (string) : boolean ) : string + child_with_name : function ( stanza_t, string, string ) : stanza_t + child_with_ns : function ( stanza_t, string, string ) : stanza_t + children : function ( stanza_t ) : children_iter, stanza_t, integer + childtags : function ( stanza_t, string, string ) : childtags_iter + maptags : function ( stanza_t, maptags_cb ) : stanza_t + find : function ( stanza_t, string ) : stanza_t | string + + top_tag : function ( stanza_t ) : string + pretty_print : function ( stanza_t ) : string + pretty_top_tag : function ( stanza_t ) : string + + -- FIXME Represent util.error support + get_error : function ( stanza_t ) : stanza_error_type, stanza_error_condition, string, stanza_t + add_error : function ( stanza_t, stanza_error_type, stanza_error_condition, string, string ) + indent : function ( stanza_t, integer, string ) : stanza_t + end + + record serialized_stanza_t + name : string + attr : { string : string } + { serialized_stanza_t | string } + end + + record message_attr + ["xml:lang"] : string + from : string + id : string + to : string + type : message_type + enum message_type + "chat" + "error" + "groupchat" + "headline" + "normal" + end + end + + record presence_attr + ["xml:lang"] : string + from : string + id : string + to : string + type : presence_type + enum presence_type + "error" + "probe" + "subscribe" + "subscribed" + "unsubscribe" + "unsubscribed" + end + end + + record iq_attr + ["xml:lang"] : string + from : string + id : string + to : string + type : iq_type + enum iq_type + "error" + "get" + "result" + "set" + end + end + + stanza : function ( string, { string : string } ) : stanza_t + is_stanza : function ( any ) : boolean + preserialize : function ( stanza_t ) : serialized_stanza_t + deserialize : function ( serialized_stanza_t ) : stanza_t + clone : function ( stanza_t, boolean ) : stanza_t + message : function ( message_attr, string ) : stanza_t + iq : function ( iq_attr ) : stanza_t + reply : function ( stanza_t ) : stanza_t + error_reply : function ( stanza_t, stanza_error_type, stanza_error_condition, string, string ) : stanza_t + presence : function ( presence_attr ) : stanza_t + xml_escape : function ( string ) : string + pretty_print : function ( string ) : string +end + +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/strbitop.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/strbitop.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,7 @@ +local record mod + sand : function (string, string) : string + sor : function (string, string) : string + sxor : function (string, string) : string + common_prefix_bits : function (string, string) : integer +end +return mod diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/struct.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/struct.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,6 @@ +local record lib + pack : function (string, ...:any) : string + unpack : function(string, string, integer) : any... + size : function(string) : integer +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/table.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/table.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,7 @@ +local record lib + create : function (narr:integer, nrec:integer):table + pack : function (...:any):{any} + move : function (table, integer, integer, integer, table) : table +end +return lib + diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/termcolours.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/termcolours.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,7 @@ +local record lib + getstring : function (string, string) : string + getstyle : function (...:string) : string + setstyle : function (string) : string + tohtml : function (string) : string +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/time.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/time.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,6 @@ + +local record lib + now : function () : number + monotonic : function () : number +end +return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/timer.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/timer.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,8 @@ +local record util_timer + record task end + type timer_callback = function (number) : number + add_task : function ( number, timer_callback, any ) : task + stop : function ( task ) + reschedule : function ( task, number ) : task +end +return util_timer diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/uuid.d.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/uuid.d.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,8 @@ +local record lib + get_nibbles : function (number) : string + generate : function () : string + + seed : function (string) +end +return lib + diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/prosody/util/xtemplate.tl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/teal-src/prosody/util/xtemplate.tl Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,109 @@ +-- render(template, stanza) --> string +-- {path} --> stanza:find(path) +-- {{ns}name/child|each({ns}name){sub-template}} + +--[[ +template ::= "{" path ("|" name ("(" args ")")? (template)? )* "}" +path ::= defined by util.stanza +name ::= %w+ +args ::= anything with balanced ( ) pairs +]] + +local s_gsub = string.gsub; +local s_match = string.match; +local s_sub = string.sub; +local t_concat = table.concat; + +local st = require "prosody.util.stanza"; + +local type escape_t = function (string) : string +local type filter_t = function (string, string | st.stanza_t, string) : string | st.stanza_t, boolean +local type filter_coll = { string : filter_t } + +local function render(template : string, root : st.stanza_t, escape : escape_t, filters : filter_coll) : string + escape = escape or st.xml_escape; + + return (s_gsub(template, "(%s*)(%b{})(%s*)", function(pre_blank : string, block : string, post_blank : string) : string + local inner = s_sub(block, 2, -2); + if inner:sub(1, 1) == "-" then + pre_blank = ""; + inner = inner:sub(2); + end + if inner:sub(-1, -1) == "-" then + post_blank = ""; + inner = inner:sub(1, -2); + end + local path, pipe, pos = s_match(inner, "^([^|]+)(|?)()"); + if not path is string then return end + local value : string | st.stanza_t + if path == "." then + value = root; + elseif path == "#" then + value = root:get_text(); + else + value = root:find(path); + end + local is_escaped = false; + + while pipe == "|" do + local func, args, tmpl, p = s_match(inner, "^(%w+)(%b())(%b{})()", pos as integer); + if not func then func, args, p = s_match(inner, "^(%w+)(%b())()", pos as integer); end + if not func then func, tmpl, p = s_match(inner, "^(%w+)(%b{})()", pos as integer); end + if not func then func, p = s_match(inner, "^(%w+)()", pos as integer); end + if not func then break end + if tmpl then tmpl = s_sub(tmpl, 2, -2); end + if args then args = s_sub(args, 2, -2); end + + if func == "each" and tmpl and st.is_stanza(value) then + if not args then value, args = root, path; end + local ns, name = s_match(args, "^(%b{})(.*)$"); + if ns then ns = s_sub(ns, 2, -2); else name, ns = args, nil; end + if ns == "" then ns = nil; end + if name == "" then name = nil; end + local out, i = {}, 1; + for c in (value as st.stanza_t):childtags(name, ns) do + out[i], i = render(tmpl, c, escape, filters), i + 1; + end + value = t_concat(out); + is_escaped = true; + elseif func == "and" and tmpl then + local condition = value; + if args then condition = root:find(args); end + if condition then + value = render(tmpl, root, escape, filters); + is_escaped = true; + end + elseif func == "or" and tmpl then + local condition = value; + if args then condition = root:find(args); end + if not condition then + value = render(tmpl, root, escape, filters); + is_escaped = true; + end + elseif filters and filters[func] then + local f = filters[func]; + if args == nil then + value, is_escaped = f(value, tmpl); + else + value, is_escaped = f(args, value, tmpl); + end + else + error("No such filter function: " .. func); + end + pipe, pos = s_match(inner, "^(|?)()", p as integer); + end + + if value is string then + if not is_escaped then value = escape(value); end + return pre_blank .. value .. post_blank; + elseif st.is_stanza(value) then + value = value:get_text(); + if value then + return pre_blank .. escape(value) .. post_blank; + end + end + return pre_blank .. post_blank; + end)); +end + +return { render = render }; diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/compat.d.tl --- a/teal-src/util/compat.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -local record lib - xpcall : function (function, function, ...:any):boolean, any -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/crand.d.tl --- a/teal-src/util/crand.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -local record lib - bytes : function (n : integer) : string - enum sourceid "OpenSSL" "arc4random()" "Linux" end - _source : sourceid -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/dataforms.d.tl --- a/teal-src/util/dataforms.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +0,0 @@ -local stanza_t = require "util.stanza".stanza_t - -local enum form_type - "form" - "submit" - "cancel" - "result" -end - -local enum field_type - "boolean" - "fixed" - "hidden" - "jid-multi" - "jid-single" - "list-multi" - "list-single" - "text-multi" - "text-private" - "text-single" -end - -local record form_field - - type : field_type - var : string -- protocol name - name : string -- internal name - - label : string - desc : string - - datatype : string - range_min : number - range_max : number - - value : any -- depends on field_type - options : table -end - -local record dataform - title : string - instructions : string - { form_field } -- XXX https://github.com/teal-language/tl/pull/415 - - form : function ( dataform, table, form_type ) : stanza_t -end - -local record lib - new : function ( dataform ) : dataform -end - -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/datamapper.tl --- a/teal-src/util/datamapper.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,381 +0,0 @@ --- Copyright (C) 2021 Kim Alvefur --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- --- Based on --- https://json-schema.org/draft/2020-12/json-schema-core.html --- https://json-schema.org/draft/2020-12/json-schema-validation.html --- http://spec.openapis.org/oas/v3.0.1#xmlObject --- https://github.com/OAI/OpenAPI-Specification/issues/630 (text:true) --- --- XML Object Extensions: --- text to refer to the text content at the same time as attributes --- x_name_is_value for enum fields where the is the value --- x_single_attribute for --- --- TODO pointers --- TODO cleanup / refactor --- TODO s/number/integer/ once we have appropriate math.type() compat --- - -local st = require "util.stanza"; -local json = require"util.json" -local pointer = require"util.jsonpointer"; - -local json_type_name = json.json_type_name; -local json_schema_object = require "util.jsonschema" -local type schema_t = boolean | json_schema_object - -local function toboolean ( s : string ) : boolean - if s == "true" or s == "1" then - return true - elseif s == "false" or s == "0" then - return false - elseif s then - return true - end -end - -local function totype(t : json_type_name, s : string) : any - if not s then return nil end - if t == "string" then - return s; - elseif t == "boolean" then - return toboolean(s) - elseif t == "number" or t == "integer" then - return tonumber(s) - end -end - -local enum value_goes - "in_tag_name" - "in_text" - "in_text_tag" - "in_attribute" - "in_single_attribute" - "in_children" - "in_wrapper" -end - -local function resolve_schema(schema : schema_t, root : json_schema_object) : schema_t - if schema is json_schema_object then - if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then - return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t; - end - end - return schema; -end - -local function guess_schema_type(schema : json_schema_object) : json_type_name - local schema_types = schema.type - if schema_types is json_type_name then - return schema_types - elseif schema_types ~= nil then - error "schema has unsupported 'type' property" - elseif schema.properties then - return "object" - elseif schema.items then - return "array" - end - return "string" -- default assumption -end - -local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string ) - : json_type_name, value_goes, string, string, string, string, { any } - local proptype : json_type_name = "string" - local value_where : value_goes = propname and "in_text_tag" or "in_text" - local name = propname - local namespace : string - local prefix : string - local single_attribute : string - local enums : { any } - - if propschema is json_schema_object then - proptype = guess_schema_type(propschema); - elseif propschema is string then -- Teal says this can never be a string, but it could before so best be sure - error("schema as string is not supported: "..propschema.." {"..current_ns.."}"..propname) - end - - if proptype == "object" or proptype == "array" then - value_where = "in_children" - end - - if propschema is json_schema_object then - local xml = propschema.xml - if xml then - if xml.name then - name = xml.name - end - if xml.namespace and xml.namespace ~= current_ns then - namespace = xml.namespace - end - if xml.prefix then - prefix = xml.prefix - end - if proptype == "array" and xml.wrapped then - value_where = "in_wrapper" - elseif xml.attribute then - value_where = "in_attribute" - elseif xml.text then - value_where = "in_text" - elseif xml.x_name_is_value then - value_where = "in_tag_name" - elseif xml.x_single_attribute then - single_attribute = xml.x_single_attribute - value_where = "in_single_attribute" - end - end - if propschema["const"] then - enums = { propschema["const"] } - elseif propschema["enum"] then - enums = propschema["enum"] - end - end - - if current_ns == "urn:xmpp:reactions:0" and name == "reactions" then - assert(proptype=="array") - end - - return proptype, value_where, name, namespace, prefix, single_attribute, enums -end - -local parse_object : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any } -local parse_array : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { any } - -local function extract_value (s : st.stanza_t, value_where : value_goes, proptype : json.json_type_name, name : string, namespace : string, prefix : string, single_attribute : string, enums : { any }) : string - if value_where == "in_tag_name" then - local c : st.stanza_t - if proptype == "boolean" then - c = s:get_child(name, namespace); - elseif enums and proptype == "string" then - -- XXX O(n²) ? - -- Probably better to flip the table and loop over :childtags(nil, ns), should be 2xO(n) - -- BUT works first, optimize later - for i = 1, #enums do - c = s:get_child(enums[i] as string, namespace); - if c then break end - end - else - c = s:get_child(nil, namespace); - end - if c then - return c.name; - end - elseif value_where == "in_attribute" then - local attr = name - if prefix then - attr = prefix .. ':' .. name - elseif namespace and namespace ~= s.attr.xmlns then - attr = namespace .. "\1" .. name - end - return s.attr[attr] - - elseif value_where == "in_text" then - return s:get_text() - - elseif value_where == "in_single_attribute" then - local c = s:get_child(name, namespace) - return c and c.attr[single_attribute] - elseif value_where == "in_text_tag" then - return s:get_child_text(name, namespace) - end -end - -function parse_object (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any } - local out : { string : any } = {} - schema = resolve_schema(schema, root) - if schema is json_schema_object and schema.properties then - for prop, propschema in pairs(schema.properties) do - propschema = resolve_schema(propschema, root) - - local proptype, value_where, name, namespace, prefix, single_attribute, enums = unpack_propschema(propschema, prop, s.attr.xmlns) - - if value_where == "in_children" and propschema is json_schema_object then - if proptype == "object" then - local c = s:get_child(name, namespace) - if c then - out[prop] = parse_object(propschema, c, root); - end - elseif proptype == "array" then - local a = parse_array(propschema, s, root); - if a and a[1] ~= nil then - out[prop] = a; - end - else - error "unreachable" - end - elseif value_where == "in_wrapper" and propschema is json_schema_object and proptype == "array" then - local wrapper = s:get_child(name, namespace); - if wrapper then - out[prop] = parse_array(propschema, wrapper, root); - end - else - local value : string = extract_value (s, value_where, proptype, name, namespace, prefix, single_attribute, enums) - - out[prop] = totype(proptype, value) - end - end - end - - return out -end - -function parse_array (schema : json_schema_object, s : st.stanza_t, root : json_schema_object) : { any } - local itemschema : schema_t = resolve_schema(schema.items, root); - local proptype, value_where, child_name, namespace, prefix, single_attribute, enums = unpack_propschema(itemschema, nil, s.attr.xmlns) - local attr_name : string - if value_where == "in_single_attribute" then -- FIXME this shouldn't be needed - value_where = "in_attribute"; - attr_name = single_attribute; - end - local out : { any } = {} - - if proptype == "object" then - if itemschema is json_schema_object then - for c in s:childtags(child_name, namespace) do - table.insert(out, parse_object(itemschema, c, root)); - end - else - error "array items must be schema object" - end - elseif proptype == "array" then - if itemschema is json_schema_object then - for c in s:childtags(child_name, namespace) do - table.insert(out, parse_array(itemschema, c, root)); - end - end - else - for c in s:childtags(child_name, namespace) do - local value : string = extract_value (c, value_where, proptype, attr_name or child_name, namespace, prefix, single_attribute, enums) - - table.insert(out, totype(proptype, value)); - end - end - return out; -end - -local function parse (schema : json_schema_object, s : st.stanza_t) : table - local s_type = guess_schema_type(schema) - if s_type == "object" then - return parse_object(schema, s, schema) - elseif s_type == "array" then - return parse_array(schema, s, schema) - else - error "top-level scalars unsupported" - end -end - -local function toxmlstring(proptype : json_type_name, v : any) : string - if proptype == "string" and v is string then - return v - elseif proptype == "number" and v is number then - return string.format("%g", v) - elseif proptype == "integer" and v is number then -- TODO is integer - return string.format("%d", v) - elseif proptype == "boolean" then - return v and "1" or "0" - end -end - -local unparse : function (json_schema_object, table, string, string, st.stanza_t, json_schema_object) : st.stanza_t - -local function unparse_property(out : st.stanza_t, v : any, proptype : json_type_name, propschema : schema_t, value_where : value_goes, name : string, namespace : string, current_ns : string, prefix : string, single_attribute : string, root : json_schema_object) - - if value_where == "in_attribute" then - local attr = name - if prefix then - attr = prefix .. ':' .. name - elseif namespace and namespace ~= current_ns then - attr = namespace .. "\1" .. name - end - - out.attr[attr] = toxmlstring(proptype, v) - elseif value_where == "in_text" then - out:text(toxmlstring(proptype, v)) - elseif value_where == "in_single_attribute" then - assert(single_attribute) - local propattr : { string : string } = {} - - if namespace and namespace ~= current_ns then - propattr.xmlns = namespace - end - - propattr[single_attribute] = toxmlstring(proptype, v) - out:tag(name, propattr):up(); - - else - local propattr : { string : string } - if namespace ~= current_ns then - propattr = { xmlns = namespace } - end - if value_where == "in_tag_name" then - if proptype == "string" and v is string then - out:tag(v, propattr):up(); - elseif proptype == "boolean" and v == true then - out:tag(name, propattr):up(); - end - elseif proptype == "object" and propschema is json_schema_object and v is table then - local c = unparse(propschema, v, name, namespace, nil, root); - if c then - out:add_direct_child(c); - end - elseif proptype == "array" and propschema is json_schema_object and v is table then - if value_where == "in_wrapper" then - local c = unparse(propschema, v, name, namespace, nil, root); - if c then - out:add_direct_child(c); - end - else - unparse(propschema, v, name, namespace, out, root); - end - else - out:text_tag(name, toxmlstring(proptype, v), propattr) - end - end -end - -function unparse ( schema : json_schema_object, t : table, current_name : string, current_ns : string, ctx : st.stanza_t, root : json_schema_object ) : st.stanza_t - - if root == nil then root = schema end - - if schema.xml then - if schema.xml.name then - current_name = schema.xml.name - end - if schema.xml.namespace then - current_ns = schema.xml.namespace - end - -- TODO prefix? - end - - local out = ctx or st.stanza(current_name, { xmlns = current_ns }) - - local s_type = guess_schema_type(schema) - if s_type == "object" then - - for prop, propschema in pairs(schema.properties) do - propschema = resolve_schema(propschema, root) - local v = t[prop] - - if v ~= nil then - local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(propschema, prop, current_ns) - unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) - end - end - return out; - - elseif s_type == "array" then - local itemschema = resolve_schema(schema.items, root) - local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns) - for _, item in ipairs(t as { string }) do - unparse_property(out, item, proptype, itemschema, value_where, name, namespace, current_ns, prefix, single_attribute, root) - end - return out; - end -end - -return { - parse = parse, - unparse = unparse, -} diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/datetime.d.tl --- a/teal-src/util/datetime.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ --- TODO s/number/integer/ once Teal gets support for that - -local record lib - date : function (t : integer) : string - datetime : function (t : integer) : string - time : function (t : integer) : string - legacy : function (t : integer) : string - parse : function (t : string) : integer -end - -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/encodings.d.tl --- a/teal-src/util/encodings.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ --- TODO many actually return Maybe(String) -local record lib - record base64 - encode : function (s : string) : string - decode : function (s : string) : string - end - record stringprep - nameprep : function (s : string, strict : boolean) : string - nodeprep : function (s : string, strict : boolean) : string - resourceprep : function (s : string, strict : boolean) : string - saslprep : function (s : string, strict : boolean) : string - end - record idna - to_ascii : function (s : string) : string - to_unicode : function (s : string) : string - end - record utf8 - valid : function (s : string) : boolean - length : function (s : string) : integer - end - record confusable - skeleton : function (s : string) : string - end - version : string -end -return lib - diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/error.d.tl --- a/teal-src/util/error.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,78 +0,0 @@ -local enum error_type - "auth" - "cancel" - "continue" - "modify" - "wait" -end - -local enum error_condition - "bad-request" - "conflict" - "feature-not-implemented" - "forbidden" - "gone" - "internal-server-error" - "item-not-found" - "jid-malformed" - "not-acceptable" - "not-allowed" - "not-authorized" - "policy-violation" - "recipient-unavailable" - "redirect" - "registration-required" - "remote-server-not-found" - "remote-server-timeout" - "resource-constraint" - "service-unavailable" - "subscription-required" - "undefined-condition" - "unexpected-request" -end - -local record protoerror - type : error_type - condition : error_condition - text : string - code : integer -end - -local record error - type : error_type - condition : error_condition - text : string - code : integer - context : { any : any } - source : string -end - -local type compact_registry_item = { string, string, string, string } -local type compact_registry = { compact_registry_item } -local type registry = { string : protoerror } -local type context = { string : any } - -local record error_registry_wrapper - source : string - registry : registry - new : function (string, context) : error - coerce : function (any, string) : any, error - wrap : function (error) : error - wrap : function (string, context) : error - is_error : function (any) : boolean -end - -local record lib - record configure_opt - auto_inject_traceback : boolean - end - new : function (protoerror, context, { string : protoerror }, string) : error - init : function (string, string, registry | compact_registry) : error_registry_wrapper - init : function (string, registry | compact_registry) : error_registry_wrapper - is_error : function (any) : boolean - coerce : function (any, string) : any, error - from_stanza : function (table, context, string) : error - configure : function -end - -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/format.d.tl --- a/teal-src/util/format.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -local record lib - format : function (string, ... : any) : string -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/hashes.d.tl --- a/teal-src/util/hashes.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -local type hash = function (msg : string, hex : boolean) : string -local type hmac = function (key : string, msg : string, hex : boolean) : string -local type kdf = function (pass : string, salt : string, i : integer) : string - -local record lib - sha1 : hash - sha256 : hash - sha224 : hash - sha384 : hash - sha512 : hash - md5 : hash - hmac_sha1 : hmac - hmac_sha256 : hmac - hmac_sha512 : hmac - hmac_md5 : hmac - scram_Hi_sha1 : kdf - pbkdf2_hmac_sha1 : kdf - pbkdf2_hmac_sha256 : kdf - equals : function (string, string) : boolean - version : string - _LIBCRYPTO_VERSION : string -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/hex.d.tl --- a/teal-src/util/hex.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -local type s2s = function (s : string) : string -local record lib - to : s2s - from : s2s -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/http.d.tl --- a/teal-src/util/http.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -local record lib - urlencode : function (s : string) : string - urldecode : function (s : string) : string - formencode : function (f : { string : string }) : string - formdecode : function (s : string) : { string : string } - contains_token : function (field : string, token : string) : boolean - normalize_path : function (path : string) : string -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/human/units.d.tl --- a/teal-src/util/human/units.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -local lib = record - adjust : function (number, string) : number, string - format : function (number, string, string) : string -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/id.d.tl --- a/teal-src/util/id.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ -local record lib - short : function () : string - medium : function () : string - long : function () : string - custom : function (integer) : function () : string - -end -return lib - diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/interpolation.d.tl --- a/teal-src/util/interpolation.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -local type renderer = function (string, { string : any }) : string -local type filter = function (string, any) : string -local record lib - new : function (string, string, funcs : { string : filter }) : renderer -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/jid.d.tl --- a/teal-src/util/jid.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,15 +0,0 @@ -local record lib - split : function (string) : string, string, string - bare : function (string) : string - prepped_split : function (string, boolean) : string, string, string - join : function (string, string, string) : string - prep : function (string, boolean) : string - compare : function (string, string) : boolean - node : function (string) : string - host : function (string) : string - resource : function (string) : string - escape : function (string) : string - unescape : function (string) : string -end - -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/json.d.tl --- a/teal-src/util/json.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,18 +0,0 @@ -local record lib - encode : function (any) : string - decode : function (string) : any, string - - enum json_type_name - "null" - "boolean" - "object" - "array" - "number" - "string" - "integer" - end - - type null_type = (nil) - null : null_type -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/jsonpointer.tl --- a/teal-src/util/jsonpointer.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ - -local enum ptr_error - "invalid-table" - "invalid-path" -end - -local function unescape_token(escaped_token : string) : string - local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~") - return unescaped -end - -local function resolve_json_pointer(ref : table, path : string) : any, ptr_error - local ptr_len = #path+1 - for part, pos in path:gmatch("/([^/]*)()") do - local token = unescape_token(part) - if not ref is table then - return nil - end - local idx = next(ref) - local new_ref : any - - if idx is string then - new_ref = ref[token] - elseif idx is integer then - local i = tonumber(token) - if token == "-" then i = #ref + 1 end - new_ref = ref[i+1] - else - return nil, "invalid-table" - end - - if pos as integer == ptr_len then - return new_ref - elseif new_ref is table then - ref = new_ref - elseif not ref is table then - return nil, "invalid-path" - end - - end - return ref -end - -return { - resolve = resolve_json_pointer, -} diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/jsonschema.tl --- a/teal-src/util/jsonschema.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,374 +0,0 @@ --- Copyright (C) 2021 Kim Alvefur --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- --- Based on --- https://json-schema.org/draft/2020-12/json-schema-core.html --- https://json-schema.org/draft/2020-12/json-schema-validation.html --- - -local json = require"util.json" -local null = json.null; - -local pointer = require "util.jsonpointer" - -local type json_type_name = json.json_type_name - --- json_type_name here is non-standard -local type schema_t = boolean | json_schema_object - -local record json_schema_object - type json_type_name = json.json_type_name - type schema_object = json_schema_object - - type : json_type_name | { json_type_name } - enum : { any } - const : any - - allOf : { schema_t } - anyOf : { schema_t } - oneOf : { schema_t } - - ["not"] : schema_t - ["if"] : schema_t - ["then"] : schema_t - ["else"] : schema_t - - ["$ref"] : string - - -- numbers - multipleOf : number - maximum : number - exclusiveMaximum : number - minimum : number - exclusiveMinimum : number - - -- strings - maxLength : integer - minLength : integer - pattern : string -- NYI - format : string - - -- arrays - prefixItems : { schema_t } - items : schema_t - contains : schema_t - maxItems : integer - minItems : integer - uniqueItems : boolean - maxContains : integer -- NYI - minContains : integer -- NYI - - -- objects - properties : { string : schema_t } - maxProperties : integer -- NYI - minProperties : integer -- NYI - required : { string } - dependentRequired : { string : { string } } - additionalProperties: schema_t - patternProperties: schema_t -- NYI - propertyNames : schema_t - - -- xml - record xml_t - name : string - namespace : string - prefix : string - attribute : boolean - wrapped : boolean - - -- nonstantard, maybe in the future - text : boolean - x_name_is_value : boolean - x_single_attribute : string - end - - xml : xml_t - - -- descriptive - title : string - description : string - deprecated : boolean - readOnly : boolean - writeOnly : boolean - - -- methods - validate : function ( schema_t, any, json_schema_object ) : boolean -end - --- TODO validator function per schema property - -local function simple_validate(schema : json_type_name | { json_type_name }, data : any) : boolean - if schema == nil then - return true - elseif schema == "object" and data is table then - return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "string") - elseif schema == "array" and data is table then - return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "number") - elseif schema == "integer" then - return math.type(data) == schema - elseif schema == "null" then - return data == null - elseif schema is { json_type_name } then - for _, one in ipairs(schema as { json_type_name }) do - if simple_validate(one, data) then - return true - end - end - return false - else - return type(data) == schema - end -end - -local complex_validate : function ( json_schema_object, any, json_schema_object ) : boolean - -local function validate (schema : schema_t, data : any, root : json_schema_object) : boolean - if schema is boolean then - return schema - else - return complex_validate(schema, data, root) - end -end - -function complex_validate (schema : json_schema_object, data : any, root : json_schema_object) : boolean - - if root == nil then - root = schema - end - - if schema["$ref"] and schema["$ref"]:sub(1,1) == "#" then - local referenced = pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t - if referenced ~= nil and referenced ~= root and referenced ~= schema then - if not validate(referenced, data, root) then - return false; - end - end - end - - if not simple_validate(schema.type, data) then - return false; - end - - if schema.type == "object" then - if data is table then - -- just check that there the keys are all strings - for k in pairs(data) do - if not k is string then - return false - end - end - end - end - - if schema.type == "array" then - if data is table then - -- just check that there the keys are all numbers - for i in pairs(data) do - if not i is integer then - return false - end - end - end - end - - if schema["enum"] ~= nil then - local match = false - for _, v in ipairs(schema["enum"]) do - if v == data then - -- FIXME supposed to do deep-compare - match = true - break - end - end - if not match then - return false - end - end - - -- XXX this is measured in byte, while JSON measures in ... bork - -- TODO use utf8.len? - if data is string then - if schema.maxLength and #data > schema.maxLength then - return false - end - if schema.minLength and #data < schema.minLength then - return false - end - end - - if data is number then - if schema.multipleOf and (data == 0 or data % schema.multipleOf ~= 0) then - return false - end - - if schema.maximum and not ( data <= schema.maximum ) then - return false - end - - if schema.exclusiveMaximum and not ( data < schema.exclusiveMaximum ) then - return false - end - - if schema.minimum and not ( data >= schema.minimum ) then - return false - end - - if schema.exclusiveMinimum and not ( data > schema.exclusiveMinimum ) then - return false - end - end - - if schema.allOf then - for _, sub in ipairs(schema.allOf) do - if not validate(sub, data, root) then - return false - end - end - end - - if schema.oneOf then - local valid = 0 - for _, sub in ipairs(schema.oneOf) do - if validate(sub, data, root) then - valid = valid + 1 - end - end - if valid ~= 1 then - return false - end - end - - if schema.anyOf then - local match = false - for _, sub in ipairs(schema.anyOf) do - if validate(sub, data, root) then - match = true - break - end - end - if not match then - return false - end - end - - if schema["not"] then - if validate(schema["not"], data, root) then - return false - end - end - - if schema["if"] ~= nil then - if validate(schema["if"], data, root) then - if schema["then"] then - return validate(schema["then"], data, root) - end - else - if schema["else"] then - return validate(schema["else"], data, root) - end - end - end - - if schema.const ~= nil and schema.const ~= data then - return false - end - - if data is table then - - if schema.maxItems and #data > schema.maxItems then - return false - end - - if schema.minItems and #data < schema.minItems then - return false - end - - if schema.required then - for _, k in ipairs(schema.required) do - if data[k] == nil then - return false - end - end - end - - if schema.propertyNames ~= nil then - for k in pairs(data) do - if not validate(schema.propertyNames, k, root) then - return false - end - end - end - - if schema.properties then - for k, sub in pairs(schema.properties) do - if data[k] ~= nil and not validate(sub, data[k], root) then - return false - end - end - end - - if schema.additionalProperties ~= nil then - for k, v in pairs(data) do - if schema.properties == nil or schema.properties[k as string] == nil then - if not validate(schema.additionalProperties, v, root) then - return false - end - end - end - end - - if schema.uniqueItems then - -- only works for scalars, would need to deep-compare for objects/arrays/tables - local values : { any : boolean } = {} - for _, v in pairs(data) do - if values[v] then - return false - end - values[v] = true - end - end - - local p = 0 - if schema.prefixItems ~= nil then - for i, s in ipairs(schema.prefixItems) do - if data[i] == nil then - break - elseif validate(s, data[i], root) then - p = i - else - return false - end - end - end - - if schema.items ~= nil then - for i = p+1, #data do - if not validate(schema.items, data[i], root) then - return false - end - end - end - - if schema.contains ~= nil then - local found = false - for i = 1, #data do - if validate(schema.contains, data[i], root) then - found = true - break - end - end - if not found then - return false - end - end - end - - return true; -end - - -json_schema_object.validate = validate; - -return json_schema_object; diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/net.d.tl --- a/teal-src/util/net.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ - -local enum type_strings - "both" - "ipv4" - "ipv6" -end - -local record lib - local_addresses : function (type_strings, boolean) : { string } - pton : function (string):string - ntop : function (string):string -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/poll.d.tl --- a/teal-src/util/poll.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -local record state - enum waiterr - "timeout" - "signal" - end - add : function (state, integer, boolean, boolean) : boolean - add : function (state, integer, boolean, boolean) : nil, string, integer - set : function (state, integer, boolean, boolean) : boolean - set : function (state, integer, boolean, boolean) : nil, string, integer - del : function (state, integer) : boolean - del : function (state, integer) : nil, string, integer - wait : function (state, integer) : integer, boolean, boolean - wait : function (state, integer) : nil, string, integer - wait : function (state, integer) : nil, waiterr - getfd : function (state) : integer -end - -local record lib - new : function () : state - EEXIST : integer - EMFILE : integer - ENOENT : integer - enum api_backend - "epoll" - "poll" - "select" - end - api : api_backend -end - -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/pposix.d.tl --- a/teal-src/util/pposix.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -local record pposix - enum syslog_facility - "auth" - "authpriv" - "cron" - "daemon" - "ftp" - "kern" - "local0" - "local1" - "local2" - "local3" - "local4" - "local5" - "local6" - "local7" - "lpr" - "mail" - "syslog" - "user" - "uucp" - end - - enum syslog_level - "debug" - "info" - "notice" - "warn" - "error" - end - - enum ulimit_resource - "CORE" - "CPU" - "DATA" - "FSIZE" - "NOFILE" - "STACK" - "MEMLOCK" - "NPROC" - "RSS" - "NICE" - end - - enum ulimit_unlimited - "unlimited" - end - - type ulimit_limit = integer | ulimit_unlimited - - record utsname - sysname : string - nodename : string - release : string - version : string - machine : string - domainname : string - end - - record memoryinfo - allocated : integer - allocated_mmap : integer - used : integer - unused : integer - returnable : integer - end - - abort : function () - - daemonize : function () : boolean, string - - syslog_open : function (ident : string, facility : syslog_facility) - syslog_close : function () - syslog_log : function (level : syslog_level, src : string, msg : string) - syslog_setminlevel : function (level : syslog_level) - - getpid : function () : integer - getuid : function () : integer - getgid : function () : integer - - setuid : function (uid : integer | string) : boolean, string -- string|integer - setgid : function (uid : integer | string) : boolean, string - initgroups : function (user : string, gid : integer) : boolean, string - - umask : function (umask : string) : string - - mkdir : function (dir : string) : boolean, string - - setrlimit : function (resource : ulimit_resource, soft : ulimit_limit, hard : ulimit_limit) : boolean, string - getrlimit : function (resource : ulimit_resource) : boolean, ulimit_limit, ulimit_limit - getrlimit : function (resource : ulimit_resource) : boolean, string - - uname : function () : utsname - - setenv : function (key : string, value : string) : boolean - - meminfo : function () : memoryinfo - - atomic_append : function (f : FILE, s : string) : boolean, string, integer - - isatty : function(FILE) : boolean - - ENOENT : integer - _NAME : string - _VESRION : string -end - -return pposix diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/random.d.tl --- a/teal-src/util/random.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -local record lib - bytes : function (n:integer):string -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/ringbuffer.d.tl --- a/teal-src/util/ringbuffer.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -local record lib - record ringbuffer - find : function (ringbuffer, string) : integer - discard : function (ringbuffer, integer) : boolean - read : function (ringbuffer, integer, boolean) : string - readuntil : function (ringbuffer, string) : string - write : function (ringbuffer, string) : integer - size : function (ringbuffer) : integer - length : function (ringbuffer) : integer - sub : function (ringbuffer, integer, integer) : string - byte : function (ringbuffer, integer, integer) : integer... - free : function (ringbuffer) : integer - end - - new : function (integer) : ringbuffer -end - -return lib - - diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/signal.d.tl --- a/teal-src/util/signal.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,41 +0,0 @@ -local record lib - enum signal - "SIGABRT" - "SIGALRM" - "SIGBUS" - "SIGCHLD" - "SIGCLD" - "SIGCONT" - "SIGFPE" - "SIGHUP" - "SIGILL" - "SIGINT" - "SIGIO" - "SIGIOT" - "SIGKILL" - "SIGPIPE" - "SIGPOLL" - "SIGPROF" - "SIGQUIT" - "SIGSEGV" - "SIGSTKFLT" - "SIGSTOP" - "SIGSYS" - "SIGTERM" - "SIGTRAP" - "SIGTTIN" - "SIGTTOU" - "SIGURG" - "SIGUSR1" - "SIGUSR2" - "SIGVTALRM" - "SIGWINCH" - "SIGXCPU" - "SIGXFSZ" - end - signal : function (integer | signal, function, boolean) : boolean - raise : function (integer | signal) - kill : function (integer, integer | signal) - -- enum : integer -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/smqueue.tl --- a/teal-src/util/smqueue.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -local queue = require "util.queue"; - -local record lib - -- T would typically be util.stanza - record smqueue - _queue : queue.queue - _head : integer - _tail : integer - - enum ack_errors - "tail" - "head" - "pop" - end - push : function (smqueue, T) - ack : function (smqueue, integer) : { T }, ack_errors - resumable : function (smqueue) : boolean - resume : function (smqueue) : queue.queue.iterator, any, integer - type consume_iter = function (smqueue) : T - consume : function (smqueue) : consume_iter - - table : function (smqueue) : { T } - end - new : function (integer) : smqueue -end - -local type smqueue = lib.smqueue; - -function smqueue:push(v) - self._head = self._head + 1; - -- Wraps instead of errors - assert(self._queue:push(v)); -end - -function smqueue:ack(h : integer) : { any }, smqueue.ack_errors - if h < self._tail then - return nil, "tail"; - elseif h > self._head then - return nil, "head"; - end - -- TODO optimize? cache table fields - local acked = {}; - self._tail = h; - local expect = self._head - self._tail; - while expect < self._queue:count() do - local v = self._queue:pop(); - if not v then return nil, "pop"; end - table.insert(acked, v); - end - return acked; -end - -function smqueue:count_unacked() : integer - return self._head - self._tail; -end - -function smqueue:count_acked() : integer - return self._tail; -end - -function smqueue:resumable() : boolean - return self._queue:count() >= (self._head - self._tail); -end - -function smqueue:resume() : queue.queue.iterator, any, integer - return self._queue:items(); -end - -function smqueue:consume() : queue.queue.consume_iter - return self._queue:consume() -end - --- Compatibility layer, plain ol' table -function smqueue:table() : { any } - local t : { any } = {}; - for i, v in self:resume() do - t[i] = v; - end - return t; -end - -local function freeze(q : smqueue) : { string:integer } - return { head = q._head, tail = q._tail } -end - -local queue_mt = { - -- - __name = "smqueue"; - __index = smqueue; - __len = smqueue.count_unacked; - __freeze = freeze; -} - -function lib.new(size : integer) : queue.queue - assert(size>0); - return setmetatable({ _head = 0; _tail = 0; _queue = queue.new(size, true) }, queue_mt); -end - -return lib; diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/stanza.d.tl --- a/teal-src/util/stanza.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -local record lib - - type children_iter = function ( stanza_t ) : stanza_t - type childtags_iter = function () : stanza_t - type maptags_cb = function ( stanza_t ) : stanza_t - - record stanza_t - name : string - attr : { string : string } - { stanza_t | string } - tags : { stanza_t } - - query : function ( stanza_t, string ) : stanza_t - body : function ( stanza_t, string, { string : string } ) : stanza_t - text_tag : function ( stanza_t, string, string, { string : string } ) : stanza_t - tag : function ( stanza_t, string, { string : string } ) : stanza_t - text : function ( stanza_t, string ) : stanza_t - up : function ( stanza_t ) : stanza_t - reset : function ( stanza_t ) : stanza_t - add_direct_child : function ( stanza_t, stanza_t ) - add_child : function ( stanza_t, stanza_t ) - remove_children : function ( stanza_t, string, string ) : stanza_t - - get_child : function ( stanza_t, string, string ) : stanza_t - get_text : function ( stanza_t ) : string - get_child_text : function ( stanza_t, string, string ) : string - child_with_name : function ( stanza_t, string, string ) : stanza_t - child_with_ns : function ( stanza_t, string, string ) : stanza_t - children : function ( stanza_t ) : children_iter, stanza_t, integer - childtags : function ( stanza_t, string, string ) : childtags_iter - maptags : function ( stanza_t, maptags_cb ) : stanza_t - find : function ( stanza_t, string ) : stanza_t | string - - top_tag : function ( stanza_t ) : string - pretty_print : function ( stanza_t ) : string - pretty_top_tag : function ( stanza_t ) : string - - get_error : function ( stanza_t ) : string, string, string, stanza_t - indent : function ( stanza_t, integer, string ) : stanza_t - end - - record serialized_stanza_t - name : string - attr : { string : string } - { serialized_stanza_t | string } - end - - stanza : function ( string, { string : string } ) : stanza_t - is_stanza : function ( any ) : boolean - preserialize : function ( stanza_t ) : serialized_stanza_t - deserialize : function ( serialized_stanza_t ) : stanza_t - clone : function ( stanza_t, boolean ) : stanza_t - message : function ( { string : string }, string ) : stanza_t - iq : function ( { string : string } ) : stanza_t - reply : function ( stanza_t ) : stanza_t - error_reply : function ( stanza_t, string, string, string, string ) - presence : function ( { string : string } ) : stanza_t - xml_escape : function ( string ) : string - pretty_print : function ( string ) : string -end - -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/strbitop.d.tl --- a/teal-src/util/strbitop.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -local record mod - sand : function (string, string) : string - sor : function (string, string) : string - sxor : function (string, string) : string -end -return mod diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/table.d.tl --- a/teal-src/util/table.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -local record lib - create : function (narr:integer, nrec:integer):table - pack : function (...:any):{any} -end -return lib - diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/time.d.tl --- a/teal-src/util/time.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ - -local record lib - now : function () : number - monotonic : function () : number -end -return lib diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/uuid.d.tl --- a/teal-src/util/uuid.d.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -local record lib - get_nibbles : (number) : string - generate : function () : string - - seed : function (string) -end -return lib - diff -r 54a936345aaa -r 5d9ec2e55d74 teal-src/util/xtemplate.tl --- a/teal-src/util/xtemplate.tl Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +0,0 @@ --- render(template, stanza) --> string --- {path} --> stanza:find(path) --- {{ns}name/child|each({ns}name){sub-template}} - ---[[ -template ::= "{" path ("|" name ("(" args ")")? (template)? )* "}" -path ::= defined by util.stanza -name ::= %w+ -args ::= anything with balanced ( ) pairs -]] - -local s_gsub = string.gsub; -local s_match = string.match; -local s_sub = string.sub; -local t_concat = table.concat; - -local st = require "util.stanza"; - -local type escape_t = function (string) : string -local type filter_t = function (string, string | st.stanza_t, string) : string | st.stanza_t, boolean -local type filter_coll = { string : filter_t } - -local function render(template : string, root : st.stanza_t, escape : escape_t, filters : filter_coll) : string - escape = escape or st.xml_escape; - - return (s_gsub(template, "%b{}", function(block : string) : string - local inner = s_sub(block, 2, -2); - local path, pipe, pos = s_match(inner, "^([^|]+)(|?)()"); - if not path is string then return end - local value : string | st.stanza_t - if path == "." then - value = root; - elseif path == "#" then - value = root:get_text(); - else - value = root:find(path); - end - local is_escaped = false; - - while pipe == "|" do - local func, args, tmpl, p = s_match(inner, "^(%w+)(%b())(%b{})()", pos as integer); - if not func then func, args, p = s_match(inner, "^(%w+)(%b())()", pos as integer); end - if not func then func, tmpl, p = s_match(inner, "^(%w+)(%b{})()", pos as integer); end - if not func then func, p = s_match(inner, "^(%w+)()", pos as integer); end - if not func then break end - if tmpl then tmpl = s_sub(tmpl, 2, -2); end - if args then args = s_sub(args, 2, -2); end - - if func == "each" and tmpl and st.is_stanza(value) then - if not args then value, args = root, path; end - local ns, name = s_match(args, "^(%b{})(.*)$"); - if ns then ns = s_sub(ns, 2, -2); else name, ns = args, nil; end - if ns == "" then ns = nil; end - if name == "" then name = nil; end - local out, i = {}, 1; - for c in (value as st.stanza_t):childtags(name, ns) do - out[i], i = render(tmpl, c, escape, filters), i + 1; - end - value = t_concat(out); - is_escaped = true; - elseif func == "and" and tmpl then - local condition = value; - if args then condition = root:find(args); end - if condition then - value = render(tmpl, root, escape, filters); - is_escaped = true; - end - elseif func == "or" and tmpl then - local condition = value; - if args then condition = root:find(args); end - if not condition then - value = render(tmpl, root, escape, filters); - is_escaped = true; - end - elseif filters and filters[func] then - local f = filters[func]; - if args == nil then - value, is_escaped = f(value, tmpl); - else - value, is_escaped = f(args, value, tmpl); - end - else - error("No such filter function: " .. func); - end - pipe, pos = s_match(inner, "^(|?)()", p as integer); - end - - if value is string then - if not is_escaped then value = escape(value); end - return value; - elseif st.is_stanza(value) then - value = value:get_text(); - if value then - return escape(value); - end - end - return ""; - end)); -end - -return { render = render }; diff -r 54a936345aaa -r 5d9ec2e55d74 tools/build-env/Containerfile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/build-env/Containerfile Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,31 @@ +ARG os +ARG dist +FROM ${os:-debian}:${dist:-sid} +ENV DEBIAN_FRONTEND noninteractive +RUN set -ex; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ccache dh-lua libicu-dev libidn11-dev libssl-dev \ + lua-bitop lua-dbi-mysql lua-dbi-postgresql lua-dbi-sqlite3 \ + lua-event lua-expat lua-filesystem lua-ldap lua-sec lua-socket \ + luarocks shellcheck mercurial; \ + apt-get install -y ca-certificates dns-root-data; \ + apt-get install -y lua-bit32 || true; \ + apt-get install -y lua-busted || true; \ + apt-get install -y lua-check || true; \ + apt-get install -y lua-readline || true; \ + apt-get install -y lua-unbound || true; \ + update-alternatives --set lua-interpreter /usr/bin/lua5.4 || true \ + apt-get clean + +# Place this file in an empty directory and build the image with +# podman build . -t prosody.im/build-env +# +# Substituting podman for docker should work, where that is what's available. +# +# Then in a source directory, run: +# podman run -it --rm -v "$PWD:$PWD" -w "$PWD" --entrypoint /bin/bash \ +# --userns=keep-id --network host prosody.im/build-env +# +# In the resulting environment everything required to compile and run prosody +# is available, so e.g. `./configure; make; ./prosody` should Just Work! diff -r 54a936345aaa -r 5d9ec2e55d74 tools/build-env/build.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/build-env/build.sh Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,16 @@ +#!/bin/sh -eux + +cd "$(dirname "$0")" + +containerify="$(command -v podman || command -v docker)" + +if [ -z "$containerify" ]; then + echo "podman or docker required" >&2 + exit 1 +fi + +$containerify build -f ./Containerfile --squash \ + --build-arg os="${2:-debian}" \ + --build-arg dist="${1:-testing}" \ + -t "prosody.im/build-env:${1:-testing}" + diff -r 54a936345aaa -r 5d9ec2e55d74 tools/build-env/here.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/build-env/here.sh Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,19 @@ +#!/bin/sh -eux + +tag="testing" + +if [ "$#" -gt 0 ]; then + tag="$1" + shift +fi + +containerify="$(command -v podman docker)" + +$containerify run -it --rm \ + -v "$PWD:$PWD" \ + -w "$PWD" \ + -v "$HOME/.cache:$PWD/.cache" \ + --entrypoint /bin/bash \ + --userns=keep-id \ + --network \ + host "prosody.im/build-env:$tag" "$@" diff -r 54a936345aaa -r 5d9ec2e55d74 tools/cfgdump.lua --- a/tools/cfgdump.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/cfgdump.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,21 +1,24 @@ #!/usr/bin/env lua -- cfgdump.lua prosody.cfg.lua [[host] option] +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end local s_format, print = string.format, print; local printf = function(fmt, ...) return print(s_format(fmt, ...)); end -local it = require "util.iterators"; +local it = require "prosody.util.iterators"; local function sort_anything(a, b) local typeof_a, typeof_b = type(a), type(b); if typeof_a ~= typeof_b then return typeof_a < typeof_b end return a < b -- should work for everything in a config file end -local serialization = require "util.serialization"; +local serialization = require "prosody.util.serialization"; local serialize = serialization.new and serialization.new({ unquoted = true, table_iterator = function(t) return it.sorted_pairs(t, sort_anything); end, }) or serialization.serialize; -local configmanager = require"core.configmanager"; -local startup = require "util.startup"; +local configmanager = require"prosody.core.configmanager"; +local startup = require "prosody.util.startup"; startup.set_function_metatable(); local config_filename, onlyhost, onlyoption = ...; diff -r 54a936345aaa -r 5d9ec2e55d74 tools/dnsregistry.lua --- a/tools/dnsregistry.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/dnsregistry.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,5 +1,8 @@ -- Generate util/dnsregistry.lua from IANA HTTP status code registry -local xml = require "util.xml"; +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end +local xml = require "prosody.util.xml"; local registries = xml.parse(io.read("*a"), { allow_processing_instructions = true }); print("-- Source: https://www.iana.org/assignments/dns-parameters/dns-parameters.xml"); @@ -22,7 +25,7 @@ local record_desc = record:get_child_text("description"); local record_code = tonumber(record:get_child_text("value")); - if tostring(record):lower():match("reserved") or tostring(record):lower():match("reserved") then + if tostring(record):lower():match("reserved") or tostring(record):lower():match("unassigned") then record_code = nil; end diff -r 54a936345aaa -r 5d9ec2e55d74 tools/ejabberdsql2prosody.lua --- a/tools/ejabberdsql2prosody.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/ejabberdsql2prosody.lua Wed Mar 27 15:39:03 2024 +0000 @@ -16,13 +16,16 @@ package.path = package.path..";"..my_name:gsub("[^/\\]+$", "../?.lua"); package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); end +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end -local serialize = require "util.serialization".serialize; -local st = require "util.stanza"; -local parse_xml = require "util.xml".parse; -package.loaded["util.logger"] = {init = function() return function() end; end} -local dm = require "util.datamanager" +local serialize = require "prosody.util.serialization".serialize; +local st = require "prosody.util.stanza"; +local parse_xml = require "prosody.util.xml".parse; +package.loaded["prosody.util.logger"] = {init = function() return function() end; end} +local dm = require "prosody.util.datamanager" dm.set_data_path("data"); function parseFile(filename) diff -r 54a936345aaa -r 5d9ec2e55d74 tools/form2table.lua --- a/tools/form2table.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/form2table.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,7 @@ -- Read an XML dataform and spit out a serialized Lua table of it +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end local function from_stanza(stanza) local layout = { @@ -45,4 +48,5 @@ return layout; end -print("dataforms.new " .. require "util.serialization".serialize(from_stanza(require "util.xml".parse(io.read("*a"))), { unquoted = true })) +print("dataforms.new " .. require"prosody.util.serialization".serialize(from_stanza(require"prosody.util.xml".parse(io.read("*a"))), + { unquoted = true })) diff -r 54a936345aaa -r 5d9ec2e55d74 tools/http-status-codes.lua --- a/tools/http-status-codes.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/http-status-codes.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,9 @@ -- Generate net/http/codes.lua from IANA HTTP status code registry +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end -local xml = require "util.xml"; +local xml = require "prosody.util.xml"; local registry = xml.parse(io.read("*a"), { allow_processing_instructions = true }); io.write([[ diff -r 54a936345aaa -r 5d9ec2e55d74 tools/jabberd14sql2prosody.lua --- a/tools/jabberd14sql2prosody.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/jabberd14sql2prosody.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,7 @@ #!/usr/bin/env lua +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end do @@ -447,13 +450,13 @@ end package.loaded["util.logger"] = {init = function() return function() end; end} -local dm = require "util.datamanager"; +local dm = require "prosody.util.datamanager"; dm.set_data_path("data"); -local datetime = require "util.datetime"; +local datetime = require "prosody.util.datetime"; -local st = require "util.stanza"; -local parse_xml = require "util.xml".parse; +local st = require "prosody.util.stanza"; +local parse_xml = require "prosody.util.xml".parse; function store_password(username, host, password) -- create or update account for username@host diff -r 54a936345aaa -r 5d9ec2e55d74 tools/migration/migrator.cfg.lua --- a/tools/migration/migrator.cfg.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/migration/migrator.cfg.lua Wed Mar 27 15:39:03 2024 +0000 @@ -3,6 +3,7 @@ local vhost = { "accounts", "account_details", + "account_roles", "roster", "vcard", "private", @@ -12,18 +13,27 @@ "offline-archive", "pubsub_nodes-pubsub", "pep-pubsub", + "cron", + "smacks_h", } local muc = { "persistent", "config", "state", "muc_log-archive", + "cron", }; +local upload = { + "uploads-archive", + "upload_stats", + "cron", +} input { hosts = { ["example.com"] = vhost; ["conference.example.com"] = muc; + ["share.example.com"] = upload; }; type = "internal"; path = data_path; diff -r 54a936345aaa -r 5d9ec2e55d74 tools/migration/migrator/jabberd14.lua --- a/tools/migration/migrator/jabberd14.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/migration/migrator/jabberd14.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,10 @@ +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end local lfs = require "lfs"; -local st = require "util.stanza"; -local parse_xml = require "util.xml".parse; +local st = require "prosody.util.stanza"; +local parse_xml = require "prosody.util.xml".parse; local os_getenv = os.getenv; local io_open = io.open; local assert = assert; diff -r 54a936345aaa -r 5d9ec2e55d74 tools/migration/prosody-migrator.lua --- a/tools/migration/prosody-migrator.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/migration/prosody-migrator.lua Wed Mar 27 15:39:03 2024 +0000 @@ -43,7 +43,11 @@ print("If no stores are specified, 'input' and 'output' are used."); end -local startup = require "util.startup"; +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end + +local startup = require "prosody.util.startup"; do startup.parse_args({ short_params = { v = "verbose", h = "help", ["?"] = "help" }; @@ -79,7 +83,7 @@ -- Command-line parsing local options = prosody.opts; -local envloadfile = require "util.envload".envloadfile; +local envloadfile = require "prosody.util.envload".envloadfile; local config_file = options.config or default_config; local from_store = arg[1] or "input"; @@ -132,8 +136,8 @@ os.exit(1); end -local async = require "util.async"; -local server = require "net.server"; +local async = require "prosody.util.async"; +local server = require "prosody.net.server"; local watchers = { error = function (_, err) error(err); @@ -143,10 +147,10 @@ end; }; -local cm = require "core.configmanager"; -local hm = require "core.hostmanager"; -local sm = require "core.storagemanager"; -local um = require "core.usermanager"; +local cm = require "prosody.core.configmanager"; +local hm = require "prosody.core.hostmanager"; +local sm = require "prosody.core.storagemanager"; +local um = require "prosody.core.usermanager"; local function users(store, host) if store.users then @@ -164,6 +168,11 @@ elseif conf.type == "sql" then cm.set(host, "sql", conf); end + if type(conf.config) == "table" then + for option, value in pairs(conf.config) do + cm.set(host, option, value); + end + end end local function get_driver(host, conf) @@ -200,7 +209,7 @@ end if options["keep-going"] then - local xpcall = require "util.xpcall".xpcall; + local xpcall = require "prosody.util.xpcall".xpcall; for t, f in pairs(migrate_once) do migrate_once[t] = function (origin, destination, user, ...) local function log_err(err) diff -r 54a936345aaa -r 5d9ec2e55d74 tools/mod2spec.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/mod2spec.sh Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,4 @@ +#!/bin/bash +set -eu + +echo "spec/${1//./_}_spec.lua" diff -r 54a936345aaa -r 5d9ec2e55d74 tools/modtrace.lua --- a/tools/modtrace.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/modtrace.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,9 +8,9 @@ -- local dbuffer = require "tools.modtrace".trace("util.dbuffer"); -- -local t_pack = require "util.table".pack; +local t_pack = table.pack; local serialize = require "util.serialization".serialize; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local set = require "util.set"; local serialize_cfg = { diff -r 54a936345aaa -r 5d9ec2e55d74 tools/openfire2prosody.lua --- a/tools/openfire2prosody.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/openfire2prosody.lua Wed Mar 27 15:39:03 2024 +0000 @@ -15,6 +15,10 @@ package.cpath = package.cpath..";"..my_name:gsub("[^/\\]+$", "../?.so"); end +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end + -- ugly workaround for getting datamanager to work outside of prosody :( prosody = { }; prosody.platform = "unknown"; @@ -24,12 +28,12 @@ prosody.platform = "posix"; end -local parse_xml = require "util.xml".parse; +local parse_xml = require "prosody.util.xml".parse; ----------------------------------------------------------------------- package.loaded["util.logger"] = {init = function() return function() end; end} -local dm = require "util.datamanager" +local dm = require "prosody.util.datamanager" dm.set_data_path("data"); local arg = ...; diff -r 54a936345aaa -r 5d9ec2e55d74 tools/tb2err --- a/tools/tb2err Wed Mar 27 15:35:15 2024 +0000 +++ b/tools/tb2err Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,7 @@ -#!/usr/bin/env lua-any --- Lua-Versions: 5.3 5.2 5.1 +#!/usr/bin/env lua -- traceback to errors.err for vim -q +-- e.g. curl https://prosody.im/paste/xxx | tb2err > errors.err && vim -q + local path_sep = package.config:sub(1,1); for line in io.lines() do local src, err = line:match("%s*(%S+)(:%d+: .*)") @@ -10,11 +11,13 @@ or src:match("/()net/") or src:match("/()util/") or src:match("/()modules/") + or src:match("/()prosody%-modules/") or src:match("/()plugins/") - or src:match("/()prosody[ctl]*$") + or src:match("/()prosody[ctl]*$") if cut then src = src:sub(cut); end + src = src:gsub("prosody%-modules/", "../modules/") src = src:gsub("^modules/", "plugins/") io.write(src, err, "\n"); end diff -r 54a936345aaa -r 5d9ec2e55d74 tools/test_mutants.sh.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/test_mutants.sh.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,217 @@ +#!/bin/bash + +POLYGLOT=1--[===[ + +set -o pipefail + +if [[ "$#" == "0" ]]; then + echo "Lua mutation testing tool" + echo + echo "Usage:" + echo " $BASH_SOURCE MODULE_NAME SPEC_FILE" + echo + echo "Requires 'lua', 'ltokenp' and 'busted' in PATH" + exit 1; +fi + +MOD_NAME="$1" +MOD_FILE="$(lua "$BASH_SOURCE" resolve "$MOD_NAME")" + +if [[ "$MOD_FILE" == "" || ! -f "$MOD_FILE" ]]; then + echo "EE: Failed to locate module '$MOD_NAME' ($MOD_FILE)"; + exit 1; +fi + +SPEC_FILE="$2" + +if [[ "$SPEC_FILE" == "" ]]; then + SPEC_FILE="spec/${MOD_NAME/./_}_spec.lua" +fi + +if [[ "$SPEC_FILE" == "" || ! -f "$SPEC_FILE" ]]; then + echo "EE: Failed to find test spec file ($SPEC_FILE)" + exit 1; +fi + +if ! busted --helper=loader "$SPEC_FILE"; then + echo "EE: Tests fail on original source. Fix it"\!; + exit 1; +fi + +export MUTANT_N=0 +LIVING_MUTANTS=0 + +FILE_PREFIX="${MOD_FILE%.*}.mutant-" +FILE_SUFFIX=".${MOD_FILE##*.}" + +gen_mutant () { + echo "Generating mutant $2 to $3..." + ltokenp -s "$BASH_SOURCE" "$1" > "$3" + return "$?" +} + +# $1 = MOD_NAME, $2 = MUTANT_N, $3 = SPEC_FILE +test_mutant () { + ( + ulimit -m 131072 # 128MB + ulimit -t 16 # 16s + ulimit -f 32768 # 128MB (?) + exec busted --helper="$BASH_SOURCE" -Xhelper mutate="$1":"$2" "$3" + ) >/dev/null + return "$?"; +} + +MUTANT_FILE="${FILE_PREFIX}${MUTANT_N}${FILE_SUFFIX}" + +gen_mutant "$MOD_FILE" "$MUTANT_N" "$MUTANT_FILE" +while [[ "$?" == "0" ]]; do + if ! test_mutant "$MOD_NAME" "$MUTANT_N" "$SPEC_FILE"; then + echo "Tests successfully killed mutant $MUTANT_N"; + rm "$MUTANT_FILE"; + else + echo "Mutant $MUTANT_N lives on"\! + LIVING_MUTANTS=$((LIVING_MUTANTS+1)) + fi + MUTANT_N=$((MUTANT_N+1)) + MUTANT_FILE="${FILE_PREFIX}${MUTANT_N}${FILE_SUFFIX}" + gen_mutant "$MOD_FILE" "$MUTANT_N" "$MUTANT_FILE" +done + +if [[ "$?" != "2" ]]; then + echo "Failed: $?" + exit "$?"; +fi + +MUTANT_SCORE="$(lua -e "print(('%0.2f'):format((1-($LIVING_MUTANTS/$MUTANT_N))*100))")" +if test -f mutant-scores.txt; then + echo "$MOD_NAME $MUTANT_SCORE" >> mutant-scores.txt +fi +echo "$MOD_NAME: All $MUTANT_N mutants generated, $LIVING_MUTANTS survived (score: $MUTANT_SCORE%)" +rm "$MUTANT_FILE"; # Last file is always unmodified +exit 0; +]===] + +-- busted helper that runs mutations +if arg then + if arg[1] == "resolve" then + local filename = package.searchpath(assert(arg[2], "no module name given"), package.path); + if filename then + print(filename); + end + os.exit(filename and 0 or 1); + end + local mutants = {}; + + for i = 1, #arg do + local opt = arg[i]; + print("LOAD", i, opt) + local module_name, mutant_n = opt:match("^mutate=([^:]+):(%d+)"); + if module_name then + mutants[module_name] = tonumber(mutant_n); + end + end + + local orig_lua_searcher = package.searchers[2]; + + local function mutant_searcher(module_name) + local mutant_n = mutants[module_name]; + if not mutant_n then + return orig_lua_searcher(module_name); + end + local base_file, err = package.searchpath(module_name, package.path); + if not base_file then + return base_file, err; + end + local mutant_file = base_file:gsub("%.lua$", (".mutant-%d.lua"):format(mutant_n)); + return loadfile(mutant_file), mutant_file; + end + + if next(mutants) then + table.insert(package.searchers, 1, mutant_searcher); + end +end + +-- filter for ltokenp to mutate scripts +do + local last_output = {}; + local function emit(...) + last_output = {...}; + io.write(...) + io.write(" ") + return true; + end + + local did_mutate = false; + local count = -1; + local threshold = tonumber(os.getenv("MUTANT_N")) or 0; + local function should_mutate() + count = count + 1; + return count == threshold; + end + + local function mutate(name, value) + if name == "if" then + -- Bypass conditionals + if should_mutate() then + return emit("if true or"); + elseif should_mutate() then + return emit("if false and"); + end + elseif name == "" then + -- Introduce off-by-one errors + if should_mutate() then + return emit(("%d"):format(tonumber(value)+1)); + elseif should_mutate() then + return emit(("%d"):format(tonumber(value)-1)); + end + elseif name == "and" then + if should_mutate() then + return emit("or"); + end + elseif name == "or" then + if should_mutate() then + return emit("and"); + end + end + end + + local current_line_n, current_line_input, current_line_output = 0, {}, {}; + function FILTER(line_n,token,name,value) + if current_line_n ~= line_n then -- Finished a line, moving to the next? + if did_mutate and did_mutate.line == current_line_n then + -- The line we finished was mutated. Store the original and modified outputs. + did_mutate.line_original_src = table.concat(current_line_input, " "); + did_mutate.line_modified_src = table.concat(current_line_output, " "); + end + current_line_input = {}; + current_line_output = {}; + end + current_line_n = line_n; + if name == "" then return; end + if name == "" then + if not did_mutate then + return os.exit(2); + else + emit(("\n-- Mutated line %d (changed '%s' to '%s'):\n"):format(did_mutate.line, did_mutate.original, did_mutate.modified)) + emit( ("-- Original: %s\n"):format(did_mutate.line_original_src)) + emit( ("-- Modified: %s\n"):format(did_mutate.line_modified_src)); + return; + end + end + if name == "" then + value = string.format("%q",value); + end + if mutate(name, value) then + did_mutate = { + original = value; + modified = table.concat(last_output); + line = line_n; + }; + else + emit(value); + end + table.insert(current_line_input, value); + table.insert(current_line_output, table.concat(last_output)); + end +end + diff -r 54a936345aaa -r 5d9ec2e55d74 tools/xepchanges.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/xepchanges.sh Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,14 @@ +#!/bin/sh -eu + +wget -N https://xmpp.org/extensions/xeplist.xml +xml2 xepinfos.csv + +xml2 < doc/doap.xml | + 2csv -d ' ' xmpp:SupportedXep @rdf:resource xmpp:version | + sed -r 's/https?:\/\/xmpp\.org\/extensions\/xep-0*([1-9][0-9]*)\.html/\1/' | + while read -r xep ver ; do + grep "^$xep," xepinfos.csv | awk -F, "\$2 != \"$ver\" { print (\"XEP-\"\$1\" updated to \"\$2\" from $ver\") }" + done diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/GNUmakefile --- a/util-src/GNUmakefile Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/GNUmakefile Wed Mar 27 15:39:03 2024 +0000 @@ -8,7 +8,7 @@ ALL=encodings.so hashes.so net.so pposix.so signal.so table.so \ ringbuffer.so time.so poll.so compat.so strbitop.so \ - struct.so + struct.so crypto.so ifdef RANDOM ALL+=crand.so @@ -28,7 +28,7 @@ encodings.o: CFLAGS+=$(IDNA_FLAGS) encodings.so: LDLIBS+=$(IDNA_LIBS) -hashes.so: LDLIBS+=$(OPENSSL_LIBS) +crypto.so hashes.so: LDLIBS+=$(OPENSSL_LIBS) crand.o: CFLAGS+=-DWITH_$(RANDOM) crand.so: LDLIBS+=$(RANDOM_LIBS) diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/compat.c --- a/util-src/compat.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/compat.c Wed Mar 27 15:39:03 2024 +0000 @@ -19,7 +19,7 @@ return lua_gettop(L); } -int luaopen_util_compat(lua_State *L) { +int luaopen_prosody_util_compat(lua_State *L) { lua_createtable(L, 0, 2); { lua_pushcfunction(L, lc_xpcall); @@ -27,3 +27,7 @@ } return 1; } + +int luaopen_util_compat(lua_State *L) { + return luaopen_prosody_util_compat(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/crand.c --- a/util-src/crand.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/crand.c Wed Mar 27 15:39:03 2024 +0000 @@ -45,7 +45,7 @@ #endif /* This wasn't present before glibc 2.25 */ -int getrandom(void *buf, size_t buflen, unsigned int flags) { +static int getrandom(void *buf, size_t buflen, unsigned int flags) { return syscall(SYS_getrandom, buf, buflen, flags); } #else @@ -66,7 +66,7 @@ #define SMALLBUFSIZ 32 #endif -int Lrandom(lua_State *L) { +static int Lrandom(lua_State *L) { char smallbuf[SMALLBUFSIZ]; char *buf = &smallbuf[0]; const lua_Integer l = luaL_checkinteger(L, 1); @@ -123,10 +123,8 @@ return 1; } -int luaopen_util_crand(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +int luaopen_prosody_util_crand(lua_State *L) { luaL_checkversion(L); -#endif lua_createtable(L, 0, 2); lua_pushcfunction(L, Lrandom); @@ -144,3 +142,6 @@ return 1; } +int luaopen_util_crand(lua_State *L) { + return luaopen_prosody_util_crand(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/crypto.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util-src/crypto.c Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,622 @@ +/* Prosody IM +-- Copyright (C) 2022 Matthew Wild +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +*/ + +/* +* crypto.c +* Lua library for cryptographic operations using OpenSSL +*/ + +#include +#include + +#ifdef _MSC_VER +typedef unsigned __int32 uint32_t; +#else +#include +#endif + +#include "lua.h" +#include "lauxlib.h" +#include +#include +#include +#include +#include +#include + +#if (LUA_VERSION_NUM == 501) +#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) +#endif + +/* The max size of an encoded 'R' or 'S' value. P-521 = 521 bits = 66 bytes */ +#define MAX_ECDSA_SIG_INT_BYTES 66 + +#include "managed_pointer.h" + +#define PKEY_MT_TAG "util.crypto key" + +static BIO* new_memory_BIO(void) { + return BIO_new(BIO_s_mem()); +} + +MANAGED_POINTER_ALLOCATOR(new_managed_EVP_MD_CTX, EVP_MD_CTX*, EVP_MD_CTX_new, EVP_MD_CTX_free) +MANAGED_POINTER_ALLOCATOR(new_managed_BIO_s_mem, BIO*, new_memory_BIO, BIO_free) +MANAGED_POINTER_ALLOCATOR(new_managed_EVP_CIPHER_CTX, EVP_CIPHER_CTX*, EVP_CIPHER_CTX_new, EVP_CIPHER_CTX_free) + +#define CRYPTO_KEY_TYPE_ERR "unexpected key type: got '%s', expected '%s'" + +static EVP_PKEY* pkey_from_arg(lua_State *L, int idx, const int type, const int require_private) { + EVP_PKEY *pkey = *(EVP_PKEY**)luaL_checkudata(L, idx, PKEY_MT_TAG); + int got_type; + if(type || require_private) { + lua_getuservalue(L, idx); + if(type != 0) { + lua_getfield(L, -1, "type"); + got_type = lua_tointeger(L, -1); + if(got_type != type) { + const char *got_key_type_name = OBJ_nid2sn(got_type); + const char *want_key_type_name = OBJ_nid2sn(type); + lua_pushfstring(L, CRYPTO_KEY_TYPE_ERR, got_key_type_name, want_key_type_name); + luaL_argerror(L, idx, lua_tostring(L, -1)); + } + lua_pop(L, 1); + } + if(require_private != 0) { + lua_getfield(L, -1, "private"); + if(lua_toboolean(L, -1) != 1) { + luaL_argerror(L, idx, "private key expected, got public key only"); + } + lua_pop(L, 1); + } + lua_pop(L, 1); + } + return pkey; +} + +static int Lpkey_finalizer(lua_State *L) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + EVP_PKEY_free(pkey); + return 0; +} + +static int Lpkey_meth_get_type(lua_State *L) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + + int key_type = EVP_PKEY_id(pkey); + lua_pushstring(L, OBJ_nid2sn(key_type)); + return 1; +} + +static int base_evp_sign(lua_State *L, const int key_type, const EVP_MD *digest_type) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, (key_type!=NID_rsassaPss)?key_type:NID_rsaEncryption, 1); + luaL_Buffer sigbuf; + + size_t msg_len; + const unsigned char* msg = (unsigned char*)lua_tolstring(L, 2, &msg_len); + + size_t sig_len; + unsigned char *sig = NULL; + EVP_MD_CTX *md_ctx = new_managed_EVP_MD_CTX(L); + + if(EVP_DigestSignInit(md_ctx, NULL, digest_type, NULL, pkey) != 1) { + lua_pushnil(L); + return 1; + } + if(key_type == NID_rsassaPss) { + EVP_PKEY_CTX_set_rsa_padding(EVP_MD_CTX_pkey_ctx(md_ctx), RSA_PKCS1_PSS_PADDING); + } + if(EVP_DigestSign(md_ctx, NULL, &sig_len, msg, msg_len) != 1) { + lua_pushnil(L); + return 1; + } + + // COMPAT w/ Lua 5.1 + luaL_buffinit(L, &sigbuf); + sig = memset(luaL_prepbuffer(&sigbuf), 0, sig_len); + + if(EVP_DigestSign(md_ctx, sig, &sig_len, msg, msg_len) != 1) { + lua_pushnil(L); + } + else { + luaL_addsize(&sigbuf, sig_len); + luaL_pushresult(&sigbuf); + return 1; + } + + return 1; +} + +static int base_evp_verify(lua_State *L, const int key_type, const EVP_MD *digest_type) { + EVP_PKEY *pkey = pkey_from_arg(L, 1, (key_type!=NID_rsassaPss)?key_type:NID_rsaEncryption, 0); + + size_t msg_len; + const unsigned char *msg = (unsigned char*)luaL_checklstring(L, 2, &msg_len); + + size_t sig_len; + const unsigned char *sig = (unsigned char*)luaL_checklstring(L, 3, &sig_len); + + EVP_MD_CTX *md_ctx = EVP_MD_CTX_new(); + + if(EVP_DigestVerifyInit(md_ctx, NULL, digest_type, NULL, pkey) != 1) { + lua_pushnil(L); + goto cleanup; + } + if(key_type == NID_rsassaPss) { + EVP_PKEY_CTX_set_rsa_padding(EVP_MD_CTX_pkey_ctx(md_ctx), RSA_PKCS1_PSS_PADDING); + } + int result = EVP_DigestVerify(md_ctx, sig, sig_len, msg, msg_len); + if(result == 0) { + lua_pushboolean(L, 0); + } else if(result != 1) { + lua_pushnil(L); + } + else { + lua_pushboolean(L, 1); + } +cleanup: + EVP_MD_CTX_free(md_ctx); + return 1; +} + +static int Lpkey_meth_public_pem(lua_State *L) { + char *data; + size_t bytes; + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + BIO *bio = new_managed_BIO_s_mem(L); + if(PEM_write_bio_PUBKEY(bio, pkey)) { + bytes = BIO_get_mem_data(bio, &data); + if (bytes > 0) { + lua_pushlstring(L, data, bytes); + } + else { + lua_pushnil(L); + } + } + else { + lua_pushnil(L); + } + return 1; +} + +static int Lpkey_meth_private_pem(lua_State *L) { + char *data; + size_t bytes; + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 1); + BIO *bio = new_managed_BIO_s_mem(L); + + if(PEM_write_bio_PrivateKey(bio, pkey, NULL, NULL, 0, NULL, NULL)) { + bytes = BIO_get_mem_data(bio, &data); + if (bytes > 0) { + lua_pushlstring(L, data, bytes); + } + else { + lua_pushnil(L); + } + } + else { + lua_pushnil(L); + } + return 1; +} + +static int push_pkey(lua_State *L, EVP_PKEY *pkey, const int type, const int privkey) { + EVP_PKEY **ud = lua_newuserdata(L, sizeof(EVP_PKEY*)); + *ud = pkey; + luaL_newmetatable(L, PKEY_MT_TAG); + lua_setmetatable(L, -2); + + /* Set some info about the key and attach it as a user value */ + lua_newtable(L); + if(type != 0) { + lua_pushinteger(L, type); + lua_setfield(L, -2, "type"); + } + if(privkey != 0) { + lua_pushboolean(L, 1); + lua_setfield(L, -2, "private"); + } + lua_setuservalue(L, -2); + return 1; +} + +static int Lgenerate_ed25519_keypair(lua_State *L) { + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); + + /* Generate key */ + EVP_PKEY_keygen_init(pctx); + EVP_PKEY_keygen(pctx, &pkey); + EVP_PKEY_CTX_free(pctx); + + push_pkey(L, pkey, NID_ED25519, 1); + return 1; +} + +static int Limport_private_pem(lua_State *L) { + EVP_PKEY *pkey = NULL; + + size_t privkey_bytes; + const char* privkey_data; + BIO *bio = new_managed_BIO_s_mem(L); + + privkey_data = luaL_checklstring(L, 1, &privkey_bytes); + BIO_write(bio, privkey_data, privkey_bytes); + pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL); + if (pkey) { + push_pkey(L, pkey, EVP_PKEY_id(pkey), 1); + } + else { + lua_pushnil(L); + } + + return 1; +} + +static int Limport_public_pem(lua_State *L) { + EVP_PKEY *pkey = NULL; + + size_t pubkey_bytes; + const char* pubkey_data; + BIO *bio = new_managed_BIO_s_mem(L); + + pubkey_data = luaL_checklstring(L, 1, &pubkey_bytes); + BIO_write(bio, pubkey_data, pubkey_bytes); + pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); + if (pkey) { + push_pkey(L, pkey, EVP_PKEY_id(pkey), 0); + } + else { + lua_pushnil(L); + } + + return 1; +} + +static int Led25519_sign(lua_State *L) { + return base_evp_sign(L, NID_ED25519, NULL); +} + +static int Led25519_verify(lua_State *L) { + return base_evp_verify(L, NID_ED25519, NULL); +} + +/* encrypt(key, iv, plaintext) */ +static int Levp_encrypt(lua_State *L, const EVP_CIPHER *cipher, const unsigned char expected_key_len, const unsigned char expected_iv_len, const size_t tag_len) { + EVP_CIPHER_CTX *ctx; + luaL_Buffer ciphertext_buffer; + + size_t key_len, iv_len, plaintext_len; + int ciphertext_len, final_len; + + const unsigned char *key = (unsigned char*)luaL_checklstring(L, 1, &key_len); + const unsigned char *iv = (unsigned char*)luaL_checklstring(L, 2, &iv_len); + const unsigned char *plaintext = (unsigned char*)luaL_checklstring(L, 3, &plaintext_len); + + if(key_len != expected_key_len) { + return luaL_error(L, "key must be %d bytes", expected_key_len); + } + if(iv_len != expected_iv_len) { + return luaL_error(L, "iv must be %d bytes", expected_iv_len); + } + if(lua_gettop(L) > 3) { + return luaL_error(L, "Expected 3 arguments, got %d", lua_gettop(L)); + } + + // Create and initialise the context + ctx = new_managed_EVP_CIPHER_CTX(L); + + // Initialise the encryption operation + if(1 != EVP_EncryptInit_ex(ctx, cipher, NULL, NULL, NULL)) { + return luaL_error(L, "Error while initializing encryption engine"); + } + + // Initialise key and IV + if(1 != EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv)) { + return luaL_error(L, "Error while initializing key/iv"); + } + + luaL_buffinit(L, &ciphertext_buffer); + unsigned char *ciphertext = (unsigned char*)luaL_prepbuffsize(&ciphertext_buffer, plaintext_len+tag_len); + + if(1 != EVP_EncryptUpdate(ctx, ciphertext, &ciphertext_len, plaintext, plaintext_len)) { + return luaL_error(L, "Error while encrypting data"); + } + + /* + * Finalise the encryption. Normally ciphertext bytes may be written at + * this stage, but this does not occur in GCM mode + */ + if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + ciphertext_len, &final_len)) { + return luaL_error(L, "Error while encrypting final data"); + } + if(final_len != 0) { + return luaL_error(L, "Non-zero final data"); + } + + if(tag_len > 0) { + /* Get the tag */ + if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, tag_len, ciphertext + ciphertext_len)) { + return luaL_error(L, "Unable to read AEAD tag of encrypted data"); + } + /* Append tag */ + luaL_addsize(&ciphertext_buffer, ciphertext_len + tag_len); + } else { + luaL_addsize(&ciphertext_buffer, ciphertext_len); + } + luaL_pushresult(&ciphertext_buffer); + + return 1; +} + +static int Laes_128_gcm_encrypt(lua_State *L) { + return Levp_encrypt(L, EVP_aes_128_gcm(), 16, 12, 16); +} + +static int Laes_256_gcm_encrypt(lua_State *L) { + return Levp_encrypt(L, EVP_aes_256_gcm(), 32, 12, 16); +} + +static int Laes_256_ctr_encrypt(lua_State *L) { + return Levp_encrypt(L, EVP_aes_256_ctr(), 32, 16, 0); +} + +/* decrypt(key, iv, ciphertext) */ +static int Levp_decrypt(lua_State *L, const EVP_CIPHER *cipher, const unsigned char expected_key_len, const unsigned char expected_iv_len, const size_t tag_len) { + EVP_CIPHER_CTX *ctx; + luaL_Buffer plaintext_buffer; + + size_t key_len, iv_len, ciphertext_len; + int plaintext_len, final_len; + + const unsigned char *key = (unsigned char*)luaL_checklstring(L, 1, &key_len); + const unsigned char *iv = (unsigned char*)luaL_checklstring(L, 2, &iv_len); + const unsigned char *ciphertext = (unsigned char*)luaL_checklstring(L, 3, &ciphertext_len); + + if(key_len != expected_key_len) { + return luaL_error(L, "key must be %d bytes", expected_key_len); + } + if(iv_len != expected_iv_len) { + return luaL_error(L, "iv must be %d bytes", expected_iv_len); + } + if(ciphertext_len <= tag_len) { + return luaL_error(L, "ciphertext must be at least %d bytes (including tag)", tag_len); + } + if(lua_gettop(L) > 3) { + return luaL_error(L, "Expected 3 arguments, got %d", lua_gettop(L)); + } + + /* Create and initialise the context */ + ctx = new_managed_EVP_CIPHER_CTX(L); + + /* Initialise the decryption operation. */ + if(!EVP_DecryptInit_ex(ctx, cipher, NULL, NULL, NULL)) { + return luaL_error(L, "Error while initializing decryption engine"); + } + + /* Initialise key and IV */ + if(!EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv)) { + return luaL_error(L, "Error while initializing key/iv"); + } + + luaL_buffinit(L, &plaintext_buffer); + unsigned char *plaintext = (unsigned char*)luaL_prepbuffsize(&plaintext_buffer, ciphertext_len); + + /* + * Provide the message to be decrypted, and obtain the plaintext output. + * EVP_DecryptUpdate can be called multiple times if necessary + */ + if(!EVP_DecryptUpdate(ctx, plaintext, &plaintext_len, ciphertext, ciphertext_len-tag_len)) { + return luaL_error(L, "Error while decrypting data"); + } + + if(tag_len > 0) { + /* Set expected tag value. Works in OpenSSL 1.0.1d and later */ + if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, (unsigned char*)ciphertext + (ciphertext_len-tag_len))) { + return luaL_error(L, "Error while processing authentication tag"); + } + } + + /* + * Finalise the decryption. A positive return value indicates success, + * anything else is a failure - the plaintext is not trustworthy. + */ + int ret = EVP_DecryptFinal_ex(ctx, plaintext + plaintext_len, &final_len); + + if(ret <= 0) { + /* Verify failed */ + lua_pushnil(L); + lua_pushliteral(L, "verify-failed"); + return 2; + } + + luaL_addsize(&plaintext_buffer, plaintext_len + final_len); + luaL_pushresult(&plaintext_buffer); + return 1; +} + +static int Laes_128_gcm_decrypt(lua_State *L) { + return Levp_decrypt(L, EVP_aes_128_gcm(), 16, 12, 16); +} + +static int Laes_256_gcm_decrypt(lua_State *L) { + return Levp_decrypt(L, EVP_aes_256_gcm(), 32, 12, 16); +} + +static int Laes_256_ctr_decrypt(lua_State *L) { + return Levp_decrypt(L, EVP_aes_256_ctr(), 32, 16, 0); +} + +/* r, s = parse_ecdsa_sig(sig_der) */ +static int Lparse_ecdsa_signature(lua_State *L) { + ECDSA_SIG *sig; + size_t sig_der_len; + const unsigned char *sig_der = (unsigned char*)luaL_checklstring(L, 1, &sig_der_len); + const size_t sig_int_bytes = luaL_checkinteger(L, 2); + const BIGNUM *r, *s; + int rlen, slen; + unsigned char rb[MAX_ECDSA_SIG_INT_BYTES]; + unsigned char sb[MAX_ECDSA_SIG_INT_BYTES]; + + if(sig_int_bytes > MAX_ECDSA_SIG_INT_BYTES) { + luaL_error(L, "requested signature size exceeds supported limit"); + } + + sig = d2i_ECDSA_SIG(NULL, &sig_der, sig_der_len); + + if(sig == NULL) { + lua_pushnil(L); + return 1; + } + + ECDSA_SIG_get0(sig, &r, &s); + + rlen = BN_bn2binpad(r, rb, sig_int_bytes); + slen = BN_bn2binpad(s, sb, sig_int_bytes); + + if (rlen == -1 || slen == -1) { + ECDSA_SIG_free(sig); + luaL_error(L, "encoded integers exceed requested size"); + } + + ECDSA_SIG_free(sig); + + lua_pushlstring(L, (const char*)rb, rlen); + lua_pushlstring(L, (const char*)sb, slen); + + return 2; +} + +/* sig_der = build_ecdsa_signature(r, s) */ +static int Lbuild_ecdsa_signature(lua_State *L) { + ECDSA_SIG *sig = ECDSA_SIG_new(); + BIGNUM *r, *s; + luaL_Buffer sigbuf; + + size_t rlen, slen; + const unsigned char *rbin, *sbin; + + rbin = (unsigned char*)luaL_checklstring(L, 1, &rlen); + sbin = (unsigned char*)luaL_checklstring(L, 2, &slen); + + r = BN_bin2bn(rbin, (int)rlen, NULL); + s = BN_bin2bn(sbin, (int)slen, NULL); + + ECDSA_SIG_set0(sig, r, s); + + luaL_buffinit(L, &sigbuf); + + /* DER structure of an ECDSA signature has 7 bytes plus the integers themselves, + which may gain an extra byte once encoded */ + unsigned char *buffer = (unsigned char*)luaL_prepbuffsize(&sigbuf, (rlen+1)+(slen+1)+7); + int len = i2d_ECDSA_SIG(sig, &buffer); + luaL_addsize(&sigbuf, len); + luaL_pushresult(&sigbuf); + + ECDSA_SIG_free(sig); + + return 1; +} + +#define REG_SIGN_VERIFY(algorithm, digest) \ + { #algorithm "_" #digest "_sign", L ## algorithm ## _ ## digest ## _sign },\ + { #algorithm "_" #digest "_verify", L ## algorithm ## _ ## digest ## _verify }, + +#define IMPL_SIGN_VERIFY(algorithm, key_type, digest) \ + static int L ## algorithm ## _ ## digest ## _sign(lua_State *L) { \ + return base_evp_sign(L, key_type, EVP_ ## digest()); \ + } \ + static int L ## algorithm ## _ ## digest ## _verify(lua_State *L) { \ + return base_evp_verify(L, key_type, EVP_ ## digest()); \ + } + +IMPL_SIGN_VERIFY(ecdsa, NID_X9_62_id_ecPublicKey, sha256) +IMPL_SIGN_VERIFY(ecdsa, NID_X9_62_id_ecPublicKey, sha384) +IMPL_SIGN_VERIFY(ecdsa, NID_X9_62_id_ecPublicKey, sha512) + +IMPL_SIGN_VERIFY(rsassa_pkcs1, NID_rsaEncryption, sha256) +IMPL_SIGN_VERIFY(rsassa_pkcs1, NID_rsaEncryption, sha384) +IMPL_SIGN_VERIFY(rsassa_pkcs1, NID_rsaEncryption, sha512) + +IMPL_SIGN_VERIFY(rsassa_pss, NID_rsassaPss, sha256) +IMPL_SIGN_VERIFY(rsassa_pss, NID_rsassaPss, sha384) +IMPL_SIGN_VERIFY(rsassa_pss, NID_rsassaPss, sha512) + +static const luaL_Reg Reg[] = { + { "ed25519_sign", Led25519_sign }, + { "ed25519_verify", Led25519_verify }, + + REG_SIGN_VERIFY(ecdsa, sha256) + REG_SIGN_VERIFY(ecdsa, sha384) + REG_SIGN_VERIFY(ecdsa, sha512) + + REG_SIGN_VERIFY(rsassa_pkcs1, sha256) + REG_SIGN_VERIFY(rsassa_pkcs1, sha384) + REG_SIGN_VERIFY(rsassa_pkcs1, sha512) + + REG_SIGN_VERIFY(rsassa_pss, sha256) + REG_SIGN_VERIFY(rsassa_pss, sha384) + REG_SIGN_VERIFY(rsassa_pss, sha512) + + { "aes_128_gcm_encrypt", Laes_128_gcm_encrypt }, + { "aes_128_gcm_decrypt", Laes_128_gcm_decrypt }, + { "aes_256_gcm_encrypt", Laes_256_gcm_encrypt }, + { "aes_256_gcm_decrypt", Laes_256_gcm_decrypt }, + + { "aes_256_ctr_encrypt", Laes_256_ctr_encrypt }, + { "aes_256_ctr_decrypt", Laes_256_ctr_decrypt }, + + { "generate_ed25519_keypair", Lgenerate_ed25519_keypair }, + + { "import_private_pem", Limport_private_pem }, + { "import_public_pem", Limport_public_pem }, + + { "parse_ecdsa_signature", Lparse_ecdsa_signature }, + { "build_ecdsa_signature", Lbuild_ecdsa_signature }, + { NULL, NULL } +}; + +static const luaL_Reg KeyMethods[] = { + { "private_pem", Lpkey_meth_private_pem }, + { "public_pem", Lpkey_meth_public_pem }, + { "get_type", Lpkey_meth_get_type }, + { NULL, NULL } +}; + +static const luaL_Reg KeyMetatable[] = { + { "__gc", Lpkey_finalizer }, + { NULL, NULL } +}; + +LUALIB_API int luaopen_prosody_util_crypto(lua_State *L) { +#if (LUA_VERSION_NUM > 501) + luaL_checkversion(L); +#endif + + /* Initialize pkey metatable */ + luaL_newmetatable(L, PKEY_MT_TAG); + luaL_setfuncs(L, KeyMetatable, 0); + lua_newtable(L); + luaL_setfuncs(L, KeyMethods, 0); + lua_setfield(L, -2, "__index"); + lua_pop(L, 1); + + /* Initialize lib table */ + lua_newtable(L); + luaL_setfuncs(L, Reg, 0); + lua_pushliteral(L, "-3.14"); + lua_setfield(L, -2, "version"); +#ifdef OPENSSL_VERSION + lua_pushstring(L, OpenSSL_version(OPENSSL_VERSION)); + lua_setfield(L, -2, "_LIBCRYPTO_VERSION"); +#endif + return 1; +} + +LUALIB_API int luaopen_util_crypto(lua_State *L) { + return luaopen_prosody_util_crypto(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/encodings.c --- a/util-src/encodings.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/encodings.c Wed Mar 27 15:39:03 2024 +0000 @@ -21,9 +21,6 @@ #include "lua.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -615,10 +612,8 @@ /***************** end *****************/ -LUALIB_API int luaopen_util_encodings(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +LUALIB_API int luaopen_prosody_util_encodings(lua_State *L) { luaL_checkversion(L); -#endif #ifdef USE_STRINGPREP_ICU init_icu(); #endif @@ -651,3 +646,6 @@ lua_setfield(L, -2, "version"); return 1; } +LUALIB_API int luaopen_util_encodings(lua_State *L) { + return luaopen_prosody_util_encodings(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/hashes.c --- a/util-src/hashes.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/hashes.c Wed Mar 27 15:39:03 2024 +0000 @@ -28,13 +28,16 @@ #include #include #include +#include +#include -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif -#define HMAC_IPAD 0x36363636 -#define HMAC_OPAD 0x5c5c5c5c +/* Semi-arbitrary limit here. The actual theoretical limit +* is (255*(hash output octets)), but allocating 16KB on the +* stack when in practice we only ever request a few dozen +* bytes seems excessive. +*/ +#define MAX_HKDF_OUTPUT 256 static const char *hex_tab = "0123456789abcdef"; static void toHex(const unsigned char *in, int length, unsigned char *out) { @@ -46,94 +49,228 @@ } } -#define MAKE_HASH_FUNCTION(myFunc, func, size) \ -static int myFunc(lua_State *L) { \ - size_t len; \ - const char *s = luaL_checklstring(L, 1, &len); \ - int hex_out = lua_toboolean(L, 2); \ - unsigned char hash[size], result[size*2]; \ - func((const unsigned char*)s, len, hash); \ - if (hex_out) { \ - toHex(hash, size, result); \ - lua_pushlstring(L, (char*)result, size*2); \ - } else { \ - lua_pushlstring(L, (char*)hash, size);\ - } \ - return 1; \ +static int Levp_hash(lua_State *L, const EVP_MD *evp) { + size_t len; + unsigned int size = EVP_MAX_MD_SIZE; + const char *s = luaL_checklstring(L, 1, &len); + int hex_out = lua_toboolean(L, 2); + + unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + + if(ctx == NULL) { + goto fail; + } + + if(!EVP_DigestInit_ex(ctx, evp, NULL)) { + goto fail; + } + + if(!EVP_DigestUpdate(ctx, s, len)) { + goto fail; + } + + if(!EVP_DigestFinal_ex(ctx, hash, &size)) { + goto fail; + } + + EVP_MD_CTX_free(ctx); + + if(hex_out) { + toHex(hash, size, result); + lua_pushlstring(L, (char *)result, size * 2); + } else { + lua_pushlstring(L, (char *)hash, size); + } + + return 1; + +fail: + EVP_MD_CTX_free(ctx); + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); +} + +static int Lsha1(lua_State *L) { + return Levp_hash(L, EVP_sha1()); +} + +static int Lsha224(lua_State *L) { + return Levp_hash(L, EVP_sha224()); +} + +static int Lsha256(lua_State *L) { + return Levp_hash(L, EVP_sha256()); +} + +static int Lsha384(lua_State *L) { + return Levp_hash(L, EVP_sha384()); +} + +static int Lsha512(lua_State *L) { + return Levp_hash(L, EVP_sha512()); +} + +static int Lmd5(lua_State *L) { + return Levp_hash(L, EVP_md5()); +} + +static int Lblake2s256(lua_State *L) { + return Levp_hash(L, EVP_blake2s256()); +} + +static int Lblake2b512(lua_State *L) { + return Levp_hash(L, EVP_blake2b512()); } -MAKE_HASH_FUNCTION(Lsha1, SHA1, SHA_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha224, SHA224, SHA224_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha256, SHA256, SHA256_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha384, SHA384, SHA384_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lsha512, SHA512, SHA512_DIGEST_LENGTH) -MAKE_HASH_FUNCTION(Lmd5, MD5, MD5_DIGEST_LENGTH) +static int Lsha3_256(lua_State *L) { + return Levp_hash(L, EVP_sha3_256()); +} + +static int Lsha3_512(lua_State *L) { + return Levp_hash(L, EVP_sha3_512()); +} -struct hash_desc { - int (*Init)(void *); - int (*Update)(void *, const void *, size_t); - int (*Final)(unsigned char *, void *); - size_t digestLength; - void *ctx, *ctxo; -}; +static int Levp_hmac(lua_State *L, const EVP_MD *evp) { + unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; + size_t key_len, msg_len; + unsigned int out_len = EVP_MAX_MD_SIZE; + const char *key = luaL_checklstring(L, 1, &key_len); + const char *msg = luaL_checklstring(L, 2, &msg_len); + const int hex_out = lua_toboolean(L, 3); -#define MAKE_HMAC_FUNCTION(myFunc, evp, size, type) \ -static int myFunc(lua_State *L) { \ - unsigned char hash[size], result[2*size]; \ - size_t key_len, msg_len; \ - unsigned int out_len; \ - const char *key = luaL_checklstring(L, 1, &key_len); \ - const char *msg = luaL_checklstring(L, 2, &msg_len); \ - const int hex_out = lua_toboolean(L, 3); \ - HMAC(evp(), key, key_len, (const unsigned char*)msg, msg_len, (unsigned char*)hash, &out_len); \ - if (hex_out) { \ - toHex(hash, out_len, result); \ - lua_pushlstring(L, (char*)result, out_len*2); \ - } else { \ - lua_pushlstring(L, (char*)hash, out_len); \ - } \ - return 1; \ + if(HMAC(evp, key, key_len, (const unsigned char*)msg, msg_len, (unsigned char*)hash, &out_len) == NULL) { + goto fail; + } + + if(hex_out) { + toHex(hash, out_len, result); + lua_pushlstring(L, (char *)result, out_len * 2); + } else { + lua_pushlstring(L, (char *)hash, out_len); + } + + return 1; + +fail: + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); +} + +static int Lhmac_sha1(lua_State *L) { + return Levp_hmac(L, EVP_sha1()); } -MAKE_HMAC_FUNCTION(Lhmac_sha1, EVP_sha1, SHA_DIGEST_LENGTH, SHA_CTX) -MAKE_HMAC_FUNCTION(Lhmac_sha256, EVP_sha256, SHA256_DIGEST_LENGTH, SHA256_CTX) -MAKE_HMAC_FUNCTION(Lhmac_sha512, EVP_sha512, SHA512_DIGEST_LENGTH, SHA512_CTX) -MAKE_HMAC_FUNCTION(Lhmac_md5, EVP_md5, MD5_DIGEST_LENGTH, MD5_CTX) +static int Lhmac_sha224(lua_State *L) { + return Levp_hmac(L, EVP_sha224()); +} + +static int Lhmac_sha256(lua_State *L) { + return Levp_hmac(L, EVP_sha256()); +} + +static int Lhmac_sha384(lua_State *L) { + return Levp_hmac(L, EVP_sha384()); +} + +static int Lhmac_sha512(lua_State *L) { + return Levp_hmac(L, EVP_sha512()); +} + +static int Lhmac_md5(lua_State *L) { + return Levp_hmac(L, EVP_md5()); +} -static int Lpbkdf2_sha1(lua_State *L) { - unsigned char out[SHA_DIGEST_LENGTH]; +static int Lhmac_sha3_256(lua_State *L) { + return Levp_hmac(L, EVP_sha3_256()); +} + +static int Lhmac_sha3_512(lua_State *L) { + return Levp_hmac(L, EVP_sha3_512()); +} + +static int Lhmac_blake2s256(lua_State *L) { + return Levp_hmac(L, EVP_blake2s256()); +} + +static int Lhmac_blake2b512(lua_State *L) { + return Levp_hmac(L, EVP_blake2b512()); +} + + +static int Levp_pbkdf2(lua_State *L, const EVP_MD *evp, size_t out_len) { + unsigned char out[EVP_MAX_MD_SIZE]; size_t pass_len, salt_len; const char *pass = luaL_checklstring(L, 1, &pass_len); const unsigned char *salt = (unsigned char *)luaL_checklstring(L, 2, &salt_len); const int iter = luaL_checkinteger(L, 3); - if(PKCS5_PBKDF2_HMAC(pass, pass_len, salt, salt_len, iter, EVP_sha1(), SHA_DIGEST_LENGTH, out) == 0) { - return luaL_error(L, "PKCS5_PBKDF2_HMAC() failed"); + if(PKCS5_PBKDF2_HMAC(pass, pass_len, salt, salt_len, iter, evp, out_len, out) == 0) { + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } - lua_pushlstring(L, (char *)out, SHA_DIGEST_LENGTH); + lua_pushlstring(L, (char *)out, out_len); return 1; } +static int Lpbkdf2_sha1(lua_State *L) { + return Levp_pbkdf2(L, EVP_sha1(), SHA_DIGEST_LENGTH); +} static int Lpbkdf2_sha256(lua_State *L) { - unsigned char out[SHA256_DIGEST_LENGTH]; + return Levp_pbkdf2(L, EVP_sha256(), SHA256_DIGEST_LENGTH); +} + + +/* HKDF(length, input, salt, info) */ +static int Levp_hkdf(lua_State *L, const EVP_MD *evp) { + unsigned char out[MAX_HKDF_OUTPUT]; + + size_t input_len, salt_len, info_len; + size_t actual_out_len = luaL_checkinteger(L, 1); + const unsigned char *input = (unsigned char *)luaL_checklstring(L, 2, &input_len); + const unsigned char *salt = (unsigned char *)luaL_optlstring(L, 3, NULL, &salt_len); + const unsigned char *info = (unsigned char *)luaL_checklstring(L, 4, &info_len); - size_t pass_len, salt_len; - const char *pass = luaL_checklstring(L, 1, &pass_len); - const unsigned char *salt = (unsigned char *)luaL_checklstring(L, 2, &salt_len); - const int iter = luaL_checkinteger(L, 3); + if(actual_out_len > MAX_HKDF_OUTPUT) + return luaL_error(L, "desired output length %ul exceeds internal limit %ul", actual_out_len, MAX_HKDF_OUTPUT); + + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); - if(PKCS5_PBKDF2_HMAC(pass, pass_len, salt, salt_len, iter, EVP_sha256(), SHA256_DIGEST_LENGTH, out) == 0) { - return luaL_error(L, "PKCS5_PBKDF2_HMAC() failed"); + if (EVP_PKEY_derive_init(pctx) <= 0) + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); + + if (EVP_PKEY_CTX_set_hkdf_md(pctx, evp) <= 0) + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); + + if(salt != NULL) { + if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, salt_len) <= 0) + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); } - lua_pushlstring(L, (char *)out, SHA256_DIGEST_LENGTH); + if (EVP_PKEY_CTX_set1_hkdf_key(pctx, input, input_len) <= 0) + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); + + if (EVP_PKEY_CTX_add1_hkdf_info(pctx, info, info_len) <= 0) + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); + + if (EVP_PKEY_derive(pctx, out, &actual_out_len) <= 0) + return luaL_error(L, ERR_error_string(ERR_get_error(), NULL)); + + lua_pushlstring(L, (char *)out, actual_out_len); + return 1; } +static int Lhkdf_sha256(lua_State *L) { + return Levp_hkdf(L, EVP_sha256()); +} + +static int Lhkdf_sha384(lua_State *L) { + return Levp_hkdf(L, EVP_sha384()); +} + static int Lhash_equals(lua_State *L) { size_t len1, len2; const char *s1 = luaL_checklstring(L, 1, &len1); @@ -153,21 +290,31 @@ { "sha384", Lsha384 }, { "sha512", Lsha512 }, { "md5", Lmd5 }, + { "sha3_256", Lsha3_256 }, + { "sha3_512", Lsha3_512 }, + { "blake2s256", Lblake2s256 }, + { "blake2b512", Lblake2b512 }, { "hmac_sha1", Lhmac_sha1 }, + { "hmac_sha224", Lhmac_sha224 }, { "hmac_sha256", Lhmac_sha256 }, + { "hmac_sha384", Lhmac_sha384 }, { "hmac_sha512", Lhmac_sha512 }, { "hmac_md5", Lhmac_md5 }, + { "hmac_sha3_256", Lhmac_sha3_256 }, + { "hmac_sha3_512", Lhmac_sha3_512 }, + { "hmac_blake2s256", Lhmac_blake2s256 }, + { "hmac_blake2b512", Lhmac_blake2b512 }, { "scram_Hi_sha1", Lpbkdf2_sha1 }, /* COMPAT */ { "pbkdf2_hmac_sha1", Lpbkdf2_sha1 }, { "pbkdf2_hmac_sha256", Lpbkdf2_sha256 }, + { "hkdf_hmac_sha256", Lhkdf_sha256 }, + { "hkdf_hmac_sha384", Lhkdf_sha384 }, { "equals", Lhash_equals }, { NULL, NULL } }; -LUALIB_API int luaopen_util_hashes(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +LUALIB_API int luaopen_prosody_util_hashes(lua_State *L) { luaL_checkversion(L); -#endif lua_newtable(L); luaL_setfuncs(L, Reg, 0); lua_pushliteral(L, "-3.14"); @@ -178,3 +325,6 @@ #endif return 1; } +LUALIB_API int luaopen_util_hashes(lua_State *L) { + return luaopen_prosody_util_hashes(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/managed_pointer.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util-src/managed_pointer.h Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,61 @@ +/* managed_pointer.h + +These macros allow wrapping an allocator/deallocator into an object that is +owned and managed by the Lua garbage collector. + +Why? It is too easy to leak objects that need to be manually released, especially +when dealing with the Lua API which can throw errors from many operations. + +USAGE +----- + +For example, given an object that can be created or released with the following +functions: + + fancy_buffer* new_buffer(); + void free_buffer(fancy_buffer* p_buffer) + +You could declare a managed version like so: + + MANAGED_POINTER_ALLOCATOR(new_managed_buffer, fancy_buffer*, new_buffer, free_buffer) + +And then, when you need to create a new fancy_buffer in your code: + + fancy_buffer *my_buffer = new_managed_buffer(L); + +NOTES +----- + +Managed objects MUST NOT be freed manually. They will automatically be +freed during the next GC sweep after your function exits (even if via an error). + +The managed object is pushed onto the stack, but should generally be ignored, +but you'll need to bear this in mind when creating managed pointers in the +middle of a sequence of stack operations. +*/ + +#define MANAGED_POINTER_MT(wrapped_type) #wrapped_type "_managedptr_mt" + +#define MANAGED_POINTER_ALLOCATOR(name, wrapped_type, wrapped_alloc, wrapped_free) \ + static int _release_ ## name(lua_State *L) { \ + wrapped_type *p = (wrapped_type*)lua_topointer(L, 1); \ + if(*p != NULL) { \ + wrapped_free(*p); \ + } \ + return 0; \ + } \ + static wrapped_type name(lua_State *L) { \ + wrapped_type *p = (wrapped_type*)lua_newuserdata(L, sizeof(wrapped_type)); \ + if(luaL_newmetatable(L, MANAGED_POINTER_MT(wrapped_type)) != 0) { \ + lua_pushcfunction(L, _release_ ## name); \ + lua_setfield(L, -2, "__gc"); \ + } \ + lua_setmetatable(L, -2); \ + *p = wrapped_alloc(); \ + if(*p == NULL) { \ + lua_pushliteral(L, "not enough memory"); \ + lua_error(L); \ + } \ + return *p; \ + } + diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/net.c --- a/util-src/net.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/net.c Wed Mar 27 15:39:03 2024 +0000 @@ -30,9 +30,6 @@ #include #include -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -192,10 +189,8 @@ return 1; } -int luaopen_util_net(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +int luaopen_prosody_util_net(lua_State *L) { luaL_checkversion(L); -#endif luaL_Reg exports[] = { { "local_addresses", lc_local_addresses }, { "pton", lc_pton }, @@ -207,3 +202,6 @@ luaL_setfuncs(L, exports, 0); return 1; } +int luaopen_util_net(lua_State *L) { + return luaopen_prosody_util_net(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/poll.c --- a/util-src/poll.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/poll.c Wed Mar 27 15:39:03 2024 +0000 @@ -8,7 +8,6 @@ * */ -#include #include #include @@ -24,15 +23,18 @@ #endif #ifdef USE_EPOLL +#include #include #ifndef MAX_EVENTS -#define MAX_EVENTS 64 +/* Maximum number of returned events, retrieved into Lpoll_state */ +#define MAX_EVENTS 256 #endif #endif #ifdef USE_POLL #include -#ifndef MAX_EVENTS -#define MAX_EVENTS 10000 +#ifndef MAX_WATCHED +/* Maximum number of watched sockets, kept in Lpoll_state */ +#define MAX_WATCHED 10000 #endif #endif #ifdef USE_SELECT @@ -44,9 +46,6 @@ #define STATE_MT "util.poll<" POLL_BACKEND ">" -#if (LUA_VERSION_NUM == 501) -#define luaL_setmetatable(L, tname) luaL_getmetatable(L, tname); lua_setmetatable(L, -2) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -62,7 +61,7 @@ #endif #ifdef USE_POLL nfds_t count; - struct pollfd events[MAX_EVENTS]; + struct pollfd events[MAX_WATCHED]; #endif #ifdef USE_SELECT fd_set wantread; @@ -123,7 +122,7 @@ } } - if(state->count >= MAX_EVENTS) { + if(state->count >= MAX_WATCHED) { luaL_pushfail(L); lua_pushstring(L, strerror(EMFILE)); lua_pushinteger(L, EMFILE); @@ -414,6 +413,12 @@ lua_Number timeout = luaL_checknumber(L, 2); luaL_argcheck(L, timeout >= 0, 1, "positive number expected"); + if(timeout == 0.0) { + lua_pushnil(L); + lua_pushstring(L, "timeout"); + return 2; + } + #ifdef USE_EPOLL ret = epoll_wait(state->epoll_fd, state->events, MAX_EVENTS, timeout * 1000); #endif @@ -540,7 +545,7 @@ state->processed = -1; state->count = 0; - for(nfds_t i = 0; i < MAX_EVENTS; i++) { + for(nfds_t i = 0; i < MAX_WATCHED; i++) { state->events[i].fd = -1; state->events[i].events = 0; state->events[i].revents = 0; @@ -563,10 +568,8 @@ /* * Open library */ -int luaopen_util_poll(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +int luaopen_prosody_util_poll(lua_State *L) { luaL_checkversion(L); -#endif luaL_newmetatable(L, STATE_MT); { @@ -619,3 +622,8 @@ return 1; } +/* COMPAT */ +int luaopen_util_poll(lua_State *L) { + return luaopen_prosody_util_poll(L); +} + diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/pposix.c --- a/util-src/pposix.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/pposix.c Wed Mar 27 15:39:03 2024 +0000 @@ -58,9 +58,6 @@ #include "lualib.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 503) #define lua_isinteger(L, n) lua_isnumber(L, n) #endif @@ -510,55 +507,43 @@ * Example usage: * pposix.setrlimit("NOFILE", 1000, 2000) */ -static int string2resource(const char *s) { - if(!strcmp(s, "CORE")) { - return RLIMIT_CORE; - } - if(!strcmp(s, "CPU")) { - return RLIMIT_CPU; - } - - if(!strcmp(s, "DATA")) { - return RLIMIT_DATA; - } - - if(!strcmp(s, "FSIZE")) { - return RLIMIT_FSIZE; - } - - if(!strcmp(s, "NOFILE")) { - return RLIMIT_NOFILE; - } - - if(!strcmp(s, "STACK")) { - return RLIMIT_STACK; - } - +static const char *const resource_strings[] = { + /* Defined by POSIX */ + "CORE", + "CPU", + "DATA", + "FSIZE", + "NOFILE", + "STACK", #if !(defined(sun) || defined(__sun) || defined(__APPLE__)) - - if(!strcmp(s, "MEMLOCK")) { - return RLIMIT_MEMLOCK; - } - - if(!strcmp(s, "NPROC")) { - return RLIMIT_NPROC; - } - - if(!strcmp(s, "RSS")) { - return RLIMIT_RSS; - } - + "MEMLOCK", + "NPROC", + "RSS", #endif #ifdef RLIMIT_NICE + "NICE", +#endif + NULL +}; - if(!strcmp(s, "NICE")) { - return RLIMIT_NICE; - } - +static int resource_constants[] = { + RLIMIT_CORE, + RLIMIT_CPU, + RLIMIT_DATA, + RLIMIT_FSIZE, + RLIMIT_NOFILE, + RLIMIT_STACK, +#if !(defined(sun) || defined(__sun) || defined(__APPLE__)) + RLIMIT_MEMLOCK, + RLIMIT_NPROC, + RLIMIT_RSS, #endif - return -1; -} +#ifdef RLIMIT_NICE + RLIMIT_NICE, +#endif + -1, +}; static rlim_t arg_to_rlimit(lua_State *L, int idx, rlim_t current) { switch(lua_type(L, idx)) { @@ -592,7 +577,7 @@ return 2; } - rid = string2resource(luaL_checkstring(L, 1)); + rid = resource_constants[luaL_checkoption(L, 1,NULL, resource_strings)]; if(rid == -1) { lua_pushboolean(L, 0); @@ -622,7 +607,6 @@ static int lc_getrlimit(lua_State *L) { int arguments = lua_gettop(L); - const char *resource = NULL; int rid = -1; struct rlimit lim; @@ -632,8 +616,7 @@ return 2; } - resource = luaL_checkstring(L, 1); - rid = string2resource(resource); + rid = resource_constants[luaL_checkoption(L, 1, NULL, resource_strings)]; if(rid != -1) { if(getrlimit(rid, &lim)) { @@ -819,6 +802,41 @@ return 3; } +static int lc_remove_blocks(lua_State *L) { +#if defined(__linux__) + int err; + + FILE *f = *(FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE); + off_t offset = (off_t)luaL_checkinteger(L, 2); + off_t length = (off_t)luaL_checkinteger(L, 3); + + errno = 0; + + if((err = fallocate(fileno(f), FALLOC_FL_COLLAPSE_RANGE, offset, length))) { + if(errno != 0) { + /* Some old versions of Linux apparently use the return value instead of errno */ + err = errno; + } + + switch(err) { + default: /* Other issues */ + luaL_pushfail(L); + lua_pushstring(L, strerror(err)); + lua_pushinteger(L, err); + return 3; + } + } + + lua_pushboolean(L, err == 0); + return 1; +#else + luaL_pushfail(L); + lua_pushstring(L, strerror(EOPNOTSUPP)); + lua_pushinteger(L, EOPNOTSUPP); + return 3; +#endif +} + static int lc_isatty(lua_State *L) { FILE *f = *(FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE); const int fd = fileno(f); @@ -828,10 +846,8 @@ /* Register functions */ -int luaopen_util_pposix(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +int luaopen_prosody_util_pposix(lua_State *L) { luaL_checkversion(L); -#endif luaL_Reg exports[] = { { "abort", lc_abort }, @@ -866,6 +882,7 @@ #endif { "atomic_append", lc_atomic_append }, + { "remove_blocks", lc_remove_blocks }, { "isatty", lc_isatty }, @@ -888,3 +905,6 @@ return 1; } +int luaopen_util_pposix(lua_State *L) { + return luaopen_prosody_util_pposix(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/ringbuffer.c --- a/util-src/ringbuffer.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/ringbuffer.c Wed Mar 27 15:39:03 2024 +0000 @@ -313,10 +313,8 @@ return 1; } -int luaopen_util_ringbuffer(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +int luaopen_prosody_util_ringbuffer(lua_State *L) { luaL_checkversion(L); -#endif if(luaL_newmetatable(L, "ringbuffer_mt")) { lua_pushcfunction(L, rb_tostring); @@ -355,3 +353,7 @@ lua_setfield(L, -2, "new"); return 1; } + +int luaopen_util_ringbuffer(lua_State *L) { + return luaopen_prosody_util_ringbuffer(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/signal.c --- a/util-src/signal.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/signal.c Wed Mar 27 15:39:03 2024 +0000 @@ -32,13 +32,14 @@ #include #include +#ifdef __linux__ +#include +#include +#endif #include "lua.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 503) #define lua_isinteger(L, n) lua_isnumber(L, n) #endif @@ -371,20 +372,104 @@ #endif +#ifdef __linux__ +struct lsignalfd { + int fd; + sigset_t mask; +}; + +static int l_signalfd(lua_State *L) { + struct lsignalfd *sfd = lua_newuserdata(L, sizeof(struct lsignalfd)); + + sigemptyset(&sfd->mask); + sigaddset(&sfd->mask, luaL_checkinteger(L, 1)); + + if (sigprocmask(SIG_BLOCK, &sfd->mask, NULL) != 0) { + lua_pushnil(L); + return 1; + }; + + sfd->fd = signalfd(-1, &sfd->mask, SFD_NONBLOCK); + + if(sfd->fd == -1) { + lua_pushnil(L); + return 1; + } + + luaL_setmetatable(L, "signalfd"); + return 1; +} + +static int l_signalfd_getfd(lua_State *L) { + struct lsignalfd *sfd = luaL_checkudata(L, 1, "signalfd"); + + if (sfd->fd == -1) { + lua_pushnil(L); + return 1; + } + + lua_pushinteger(L, sfd->fd); + return 1; +} + +static int l_signalfd_read(lua_State *L) { + struct lsignalfd *sfd = luaL_checkudata(L, 1, "signalfd"); + struct signalfd_siginfo siginfo; + + if(read(sfd->fd, &siginfo, sizeof(siginfo)) < 0) { + return 0; + } + + lua_pushinteger(L, siginfo.ssi_signo); + return 1; +} + +static int l_signalfd_close(lua_State *L) { + struct lsignalfd *sfd = luaL_checkudata(L, 1, "signalfd"); + + if(close(sfd->fd) != 0) { + lua_pushboolean(L, 0); + return 1; + } + + sfd->fd = -1; + lua_pushboolean(L, 1); + return 1; +} +#endif + static const struct luaL_Reg lsignal_lib[] = { {"signal", l_signal}, {"raise", l_raise}, #if defined(__unix__) || defined(__APPLE__) {"kill", l_kill}, #endif +#ifdef __linux__ + {"signalfd", l_signalfd}, +#endif {NULL, NULL} }; -int luaopen_util_signal(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +int luaopen_prosody_util_signal(lua_State *L) { luaL_checkversion(L); + int i = 0; + +#ifdef __linux__ + luaL_newmetatable(L, "signalfd"); + lua_pushcfunction(L, l_signalfd_close); + lua_setfield(L, -2, "__gc"); + lua_createtable(L, 0, 1); + { + lua_pushcfunction(L, l_signalfd_getfd); + lua_setfield(L, -2, "getfd"); + lua_pushcfunction(L, l_signalfd_read); + lua_setfield(L, -2, "read"); + lua_pushcfunction(L, l_signalfd_close); + lua_setfield(L, -2, "close"); + } + lua_setfield(L, -2, "__index"); + lua_pop(L, 1); #endif - int i = 0; /* add the library */ lua_newtable(L); @@ -413,3 +498,6 @@ return 1; } +int luaopen_util_signal(lua_State *L) { + return luaopen_prosody_util_signal(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/strbitop.c --- a/util-src/strbitop.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/strbitop.c Wed Mar 27 15:39:03 2024 +0000 @@ -8,13 +8,12 @@ #include #include -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif +#include +#include /* TODO Deduplicate code somehow */ -int strop_and(lua_State *L) { +static int strop_and(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); @@ -35,7 +34,7 @@ return 1; } -int strop_or(lua_State *L) { +static int strop_or(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); @@ -56,7 +55,7 @@ return 1; } -int strop_xor(lua_State *L) { +static int strop_xor(lua_State *L) { luaL_Buffer buf; size_t a, b, i; const char *str_a = luaL_checklstring(L, 1, &a); @@ -77,11 +76,46 @@ return 1; } -LUA_API int luaopen_util_strbitop(lua_State *L) { +unsigned int clz(unsigned char c) { +#if __GNUC__ + return __builtin_clz((unsigned int) c) - ((sizeof(int)-1)*CHAR_BIT); +#else + if(c & 0x80) return 0; + if(c & 0x40) return 1; + if(c & 0x20) return 2; + if(c & 0x10) return 3; + if(c & 0x08) return 4; + if(c & 0x04) return 5; + if(c & 0x02) return 6; + if(c & 0x01) return 7; + return 8; +#endif +} + +LUA_API int strop_common_prefix_bits(lua_State *L) { + size_t a, b, i; + const char *str_a = luaL_checklstring(L, 1, &a); + const char *str_b = luaL_checklstring(L, 2, &b); + + size_t min_len = MIN(a, b); + + for(i=0; i= 502) - -#define luaL_register(L,n,f) luaL_newlib(L,f) - -#endif - /* basic integer type */ #if !defined(STRUCT_INT) @@ -140,7 +134,7 @@ /* -** options to control endianess and alignment +** options to control endianness and alignment */ static void controloptions (lua_State *L, int opt, const char **fmt, Header *h) { @@ -391,9 +385,13 @@ LUALIB_API int luaopen_util_struct (lua_State *L); +LUALIB_API int luaopen_prosody_util_struct (lua_State *L) { + luaL_newlib(L, thislib); + return 1; +} + LUALIB_API int luaopen_util_struct (lua_State *L) { - luaL_register(L, "struct", thislib); - return 1; + return luaopen_prosody_util_struct(L); } diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/table.c --- a/util-src/table.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/table.c Wed Mar 27 15:39:03 2024 +0000 @@ -1,11 +1,17 @@ #include #include +#ifndef LUA_MAXINTEGER +#include +#define LUA_MAXINTEGER PTRDIFF_MAX +#endif + static int Lcreate_table(lua_State *L) { lua_createtable(L, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2)); return 1; } +/* COMPAT: w/ Lua pre-5.2 */ static int Lpack(lua_State *L) { unsigned int n_args = lua_gettop(L); lua_createtable(L, n_args, 1); @@ -20,14 +26,52 @@ return 1; } -int luaopen_util_table(lua_State *L) { -#if (LUA_VERSION_NUM > 501) +/* COMPAT: w/ Lua pre-5.4 */ +static int Lmove (lua_State *L) { + lua_Integer f = luaL_checkinteger(L, 2); + lua_Integer e = luaL_checkinteger(L, 3); + lua_Integer t = luaL_checkinteger(L, 4); + + int tt = !lua_isnoneornil(L, 5) ? 5 : 1; /* destination table */ + luaL_checktype(L, 1, LUA_TTABLE); + luaL_checktype(L, tt, LUA_TTABLE); + + if (e >= f) { /* otherwise, nothing to move */ + lua_Integer n, i; + luaL_argcheck(L, f > 0 || e < LUA_MAXINTEGER + f, 3, + "too many elements to move"); + n = e - f + 1; /* number of elements to move */ + luaL_argcheck(L, t <= LUA_MAXINTEGER - n + 1, 4, + "destination wrap around"); + if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { + for (i = 0; i < n; i++) { + lua_rawgeti(L, 1, f + i); + lua_rawseti(L, tt, t + i); + } + } else { + for (i = n - 1; i >= 0; i--) { + lua_rawgeti(L, 1, f + i); + lua_rawseti(L, tt, t + i); + } + } + } + + lua_pushvalue(L, tt); /* return destination table */ + return 1; +} + +int luaopen_prosody_util_table(lua_State *L) { luaL_checkversion(L); -#endif lua_createtable(L, 0, 2); lua_pushcfunction(L, Lcreate_table); lua_setfield(L, -2, "create"); lua_pushcfunction(L, Lpack); lua_setfield(L, -2, "pack"); + lua_pushcfunction(L, Lmove); + lua_setfield(L, -2, "move"); return 1; } + +int luaopen_util_table(lua_State *L) { + return luaopen_prosody_util_table(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/time.c --- a/util-src/time.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/time.c Wed Mar 27 15:39:03 2024 +0000 @@ -23,7 +23,7 @@ return 1; } -int luaopen_util_time(lua_State *L) { +int luaopen_prosody_util_time(lua_State *L) { lua_createtable(L, 0, 2); { lua_pushcfunction(L, lc_time_realtime); @@ -33,3 +33,6 @@ } return 1; } +int luaopen_util_time(lua_State *L) { + return luaopen_prosody_util_time(L); +} diff -r 54a936345aaa -r 5d9ec2e55d74 util-src/windows.c --- a/util-src/windows.c Wed Mar 27 15:35:15 2024 +0000 +++ b/util-src/windows.c Wed Mar 27 15:39:03 2024 +0000 @@ -19,9 +19,6 @@ #include "lua.h" #include "lauxlib.h" -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif #if (LUA_VERSION_NUM < 504) #define luaL_pushfail lua_pushnil #endif @@ -106,9 +103,7 @@ }; LUALIB_API int luaopen_util_windows(lua_State *L) { -#if (LUA_VERSION_NUM > 501) luaL_checkversion(L); -#endif lua_newtable(L); luaL_setfuncs(L, Reg, 0); lua_pushliteral(L, "-3.14"); diff -r 54a936345aaa -r 5d9ec2e55d74 util/adminstream.lua --- a/util/adminstream.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/adminstream.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,15 +1,15 @@ -local st = require "util.stanza"; -local new_xmpp_stream = require "util.xmppstream".new; -local sessionlib = require "util.session"; -local gettime = require "util.time".now; -local runner = require "util.async".runner; -local add_task = require "util.timer".add_task; -local events = require "util.events"; -local server = require "net.server"; +local st = require "prosody.util.stanza"; +local new_xmpp_stream = require "prosody.util.xmppstream".new; +local sessionlib = require "prosody.util.session"; +local gettime = require "prosody.util.time".now; +local runner = require "prosody.util.async".runner; +local add_task = require "prosody.util.timer".add_task; +local events = require "prosody.util.events"; +local server = require "prosody.net.server"; local stream_close_timeout = 5; -local log = require "util.logger".init("adminstream"); +local log = require "prosody.util.logger".init("adminstream"); local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; @@ -145,7 +145,7 @@ -- constructor was exported instead of a module table. Due to the lack of a -- proper release of LuaSocket, distros have settled on shipping either the -- last RC tag or some commit since then. - -- Here we accomodate both variants. + -- Here we accommodate both variants. unix = { stream = unix }; end if type(unix) ~= "table" then diff -r 54a936345aaa -r 5d9ec2e55d74 util/argparse.lua --- a/util/argparse.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/argparse.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,7 @@ local function parse(arg, config) local short_params = config and config.short_params or {}; local value_params = config and config.value_params or {}; + local array_params = config and config.array_params or {}; local parsed_opts = {}; @@ -30,7 +31,7 @@ end local param_k, param_v; - if value_params[param] then + if value_params[param] or array_params[param] then param_k, param_v = param, table.remove(arg, 1); if not param_v then return nil, "missing-value", raw_param; @@ -44,8 +45,17 @@ param_k, param_v = param, true; end end + param_k = param_k:gsub("%-", "_"); end - parsed_opts[param_k] = param_v; + if array_params[param] then + if parsed_opts[param_k] then + table.insert(parsed_opts[param_k], param_v); + else + parsed_opts[param_k] = { param_v }; + end + else + parsed_opts[param_k] = param_v; + end end for i = 1, #arg do parsed_opts[i] = arg[i]; diff -r 54a936345aaa -r 5d9ec2e55d74 util/array.lua --- a/util/array.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/array.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,6 +8,7 @@ local t_insert, t_sort, t_remove, t_concat = table.insert, table.sort, table.remove, table.concat; +local t_move = require "prosody.util.table".move; local setmetatable = setmetatable; local getmetatable = getmetatable; @@ -23,7 +24,7 @@ local array_mt = { __index = array_methods; __name = "array"; - __tostring = function (self) return "{"..self:concat(", ").."}"; end; + __tostring = function (self) return "["..self:concat(", ").."]"; end; }; function array_mt:__freeze() return self; end @@ -141,13 +142,11 @@ return outa; end - for idx = 1, 1+j-i do - outa[idx] = ina[i+(idx-1)]; - end + + t_move(ina, i, j, 1, outa); if ina == outa then - for idx = 2+j-i, #outa do - outa[idx] = nil; - end + -- Clear (nil) remainder of range + t_move(ina, #outa+1, #outa*2, 2+j-i, ina); end return outa; end @@ -213,10 +212,7 @@ end function array_methods:append(ina) - local len, len2 = #self, #ina; - for i = 1, len2 do - self[len+i] = ina[i]; - end + t_move(ina, 1, #ina, #self+1, self); return self; end diff -r 54a936345aaa -r 5d9ec2e55d74 util/async.lua --- a/util/async.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/async.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,8 @@ -local logger = require "util.logger"; +local logger = require "prosody.util.logger"; local log = logger.init("util.async"); -local new_id = require "util.id".short; -local xpcall = require "util.xpcall".xpcall; +local new_id = require "prosody.util.id".short; +local xpcall = require "prosody.util.xpcall".xpcall; +local time_now = require "prosody.util.time".now; local function checkthread() local thread, main = coroutine.running(); @@ -45,7 +46,9 @@ local function runner_continue(thread) -- ASSUMPTION: runner is in 'waiting' state (but we don't have the runner to know for sure) if coroutine.status(thread) ~= "suspended" then -- This should suffice - log("error", "unexpected async state: thread not suspended"); + log("error", "unexpected async state: thread not suspended (%s, %s)", thread, coroutine.status(thread)); + -- Fetching the traceback is likely to *crash* if a C library is calling us while suspended + --log("error", "coroutine stack: %s", debug.traceback()); return false; end local ok, state, runner = coroutine.resume(thread); @@ -138,6 +141,8 @@ local runner_mt = {}; runner_mt.__index = runner_mt; +local waiting_runners = {}; + local function runner_create_thread(func, self) local thread = coroutine.create(function (self) -- luacheck: ignore 432/self while true do @@ -195,6 +200,8 @@ -- Loop through queue items, and attempt to run them for i = 1,n do local queued_input = q[i]; + self:log("Resuming thread with new item [%s]", thread); + self.current_item = queued_input; local ok, new_state = coroutine.resume(thread, queued_input); if not ok then -- There was an error running the coroutine, save the error, mark runner as ready to begin again @@ -221,8 +228,13 @@ end -- Runner processed all items it can, so save current runner state self.state = state; + if state == "ready" and self.current_item then + self.current_item = nil; + end + if err or state ~= self.notified_state then - self:log("debug", "changed state from %s to %s", self.notified_state, err and ("error ("..state..")") or state); + self:log("debug", "changed state from %s to %s [%s %s]", self.notified_state, err and ("error (" .. state .. ")") or state, self.thread, + self.thread and coroutine.status(self.thread)); if err then state = "error" else @@ -234,6 +246,7 @@ if n > 0 then return self:run(); end + waiting_runners[self] = state == "waiting" and time_now() or nil; return true, state, n; end @@ -293,4 +306,7 @@ set_nexttick = function(new_next_tick) next_tick = new_next_tick; end; set_schedule_function = function (new_schedule_function) schedule_task = new_schedule_function; end; + + waiting_runners = waiting_runners; + default_runner_func = default_func; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/bit53.lua --- a/util/bit53.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/bit53.lua Wed Mar 27 15:39:03 2024 +0000 @@ -27,6 +27,9 @@ end return ret; end; + bnot = function (x) + return ~x; + end; rshift = function (a, n) return a >> n end; lshift = function (a, n) return a << n end; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/bitcompat.lua --- a/util/bitcompat.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/bitcompat.lua Wed Mar 27 15:39:03 2024 +0000 @@ -5,25 +5,11 @@ -- Lua 5.2 has it by default if _G.bit32 then return _G.bit32; -else - -- Lua 5.1 may have it as a standalone module that can be installed - local ok, bitop = pcall(require, "bit32") - if ok then - return bitop; - end end do -- Lua 5.3 and 5.4 would be able to use native infix operators - local ok, bitop = pcall(require, "util.bit53") - if ok then - return bitop; - end -end - -do - -- Lastly, try the LuaJIT bitop library - local ok, bitop = pcall(require, "bit") + local ok, bitop = pcall(require, "prosody.util.bit53") if ok then return bitop; end diff -r 54a936345aaa -r 5d9ec2e55d74 util/cache.lua --- a/util/cache.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/cache.lua Wed Mar 27 15:39:03 2024 +0000 @@ -54,12 +54,17 @@ if self._count == self.size then local tail = self._tail; local on_evict, evicted_key, evicted_value = self._on_evict, tail.key, tail.value; - if on_evict ~= nil and (on_evict == false or on_evict(evicted_key, evicted_value) == false) then + + local do_evict = on_evict and on_evict(evicted_key, evicted_value, self); + + if do_evict == false then -- Cache is full, and we're not allowed to evict return false; + elseif self._count == self.size then + -- Cache wasn't grown + _remove(self, tail); + self._data[evicted_key] = nil; end - _remove(self, tail); - self._data[evicted_key] = nil; end m = { key = k, value = v, prev = nil, next = nil }; @@ -124,7 +129,7 @@ while self._count > new_size do local tail = self._tail; local evicted_key, evicted_value = tail.key, tail.value; - if on_evict ~= nil and (on_evict == false or on_evict(evicted_key, evicted_value) == false) then + if on_evict ~= nil and (on_evict == false or on_evict(evicted_key, evicted_value, self) == false) then -- Cache is full, and we're not allowed to evict return false; end diff -r 54a936345aaa -r 5d9ec2e55d74 util/caps.lua --- a/util/caps.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/caps.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,8 +6,8 @@ -- COPYING file in the source package for more information. -- -local base64 = require "util.encodings".base64.encode; -local sha1 = require "util.hashes".sha1; +local base64 = require "prosody.util.encodings".base64.encode; +local sha1 = require "prosody.util.hashes".sha1; local t_insert, t_sort, t_concat = table.insert, table.sort, table.concat; local ipairs = ipairs; diff -r 54a936345aaa -r 5d9ec2e55d74 util/dataforms.lua --- a/util/dataforms.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/dataforms.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,9 +12,9 @@ local tonumber = tonumber; local tostring = tostring; local t_concat = table.concat; -local st = require "util.stanza"; -local jid_prep = require "util.jid".prep; -local datetime = require "util.datetime"; +local st = require "prosody.util.stanza"; +local jid_prep = require "prosody.util.jid".prep; +local datetime = require "prosody.util.datetime"; local _ENV = nil; -- luacheck: std none diff -r 54a936345aaa -r 5d9ec2e55d74 util/datamanager.lua --- a/util/datamanager.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/datamanager.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,35 +7,42 @@ -- +local string = string; local format = string.format; local setmetatable = setmetatable; local ipairs = ipairs; local char = string.char; local pcall = pcall; -local log = require "util.logger".init("datamanager"); +local log = require "prosody.util.logger".init("datamanager"); local io_open = io.open; local os_remove = os.remove; local os_rename = os.rename; local tonumber = tonumber; +local floor = math.floor; local next = next; local type = type; local t_insert = table.insert; local t_concat = table.concat; -local envloadfile = require"util.envload".envloadfile; -local serialize = require "util.serialization".serialize; +local envloadfile = require"prosody.util.envload".envloadfile; +local envload = require"prosody.util.envload".envload; +local serialize = require "prosody.util.serialization".serialize; local lfs = require "lfs"; -- Extract directory separator from package.config (an undocumented string that comes with lua) local path_separator = assert ( package.config:match ( "^([^\n]+)" ) , "package.config not in standard form" ) local prosody = prosody; +--luacheck: ignore 211/blocksize 211/remove_blocks +local blocksize = 0x1000; local raw_mkdir = lfs.mkdir; local atomic_append; +local remove_blocks; local ENOENT = 2; pcall(function() - local pposix = require "util.pposix"; + local pposix = require "prosody.util.pposix"; raw_mkdir = pposix.mkdir or raw_mkdir; -- Doesn't trample on umask atomic_append = pposix.atomic_append; + -- remove_blocks = pposix.remove_blocks; ENOENT = pposix.ENOENT or ENOENT; end); @@ -239,6 +246,14 @@ end local pos = f:seek("end"); + --[[ TODO needs tests + if (blocksize-(pos%blocksize)) < (#data%blocksize) then + -- pad to blocksize with newlines so that the next item is both on a new + -- block and a new line + atomic_append(f, ("\n"):rep(blocksize-(pos%blocksize))); + pos = f:seek("end"); + end + --]] local ok, msg = atomic_append(f, data); @@ -255,6 +270,13 @@ return true, pos; end +local index_fmt, index_item_size, index_magic; +if string.packsize then + index_fmt = "T"; -- offset to the end of the item, length can be derived from two index items + index_item_size = string.packsize(index_fmt); + index_magic = string.pack(index_fmt, 7767639 + 1); -- Magic string: T9 for "prosody", version number +end + local function list_append(username, host, datastore, data) if not data then return; end if callback(username, host, datastore) == false then return true; end @@ -267,6 +289,22 @@ datastore, msg, where, username or "nil", host or "nil"); return ok, msg; end + if string.packsize then + local offset = type(msg) == "number" and msg or 0; + local index_entry = string.pack(index_fmt, offset + #data); + if offset == 0 then + index_entry = index_magic .. index_entry; + end + local ok, off = append(username, host, datastore, "lidx", index_entry); + off = off or 0; + -- If this was the first item, then both the data and index offsets should + -- be zero, otherwise there's some kind of mismatch and we should drop the + -- index and recreate it from scratch + -- TODO Actually rebuild the index in this case? + if not ok or (off == 0 and offset ~= 0) or (off ~= 0 and offset == 0) then + os_remove(getpath(username, host, datastore, "lidx")); + end + end return true; end @@ -280,6 +318,7 @@ for i, item in ipairs(data) do d[i] = "item(" .. serialize(item) .. ");\n"; end + os_remove(getpath(username, host, datastore, "lidx")); local ok, msg = atomic_store(getpath(username, host, datastore, "list", true), t_concat(d)); if not ok then log("error", "Unable to write to %s storage ('%s') for user: %s@%s", datastore, msg, username or "nil", host or "nil"); @@ -294,6 +333,160 @@ return true; end +local function build_list_index(username, host, datastore, items) + log("debug", "Building index for (%s@%s/%s)", username, host, datastore); + local filename = getpath(username, host, datastore, "list"); + local fh, err, errno = io_open(filename); + if not fh then + return fh, err, errno; + end + local prev_pos = 0; -- position before reading + local last_item_start = nil; + + if items and items[1] then + local last_item = items[#items]; + last_item_start = fh:seek("set", last_item.start + last_item.length); + else + items = {}; + end + + for line in fh:lines() do + if line:sub(1, 4) == "item" then + if prev_pos ~= 0 and last_item_start then + t_insert(items, { start = last_item_start; length = prev_pos - last_item_start }); + end + last_item_start = prev_pos + end + -- seek position is at the start of the next line within each loop iteration + -- so we need to collect the "current" position at the end of the previous + prev_pos = fh:seek() + end + fh:close(); + if prev_pos ~= 0 then + t_insert(items, { start = last_item_start; length = prev_pos - last_item_start }); + end + return items; +end + +local function store_list_index(username, host, datastore, index) + local data = { index_magic }; + for i, v in ipairs(index) do + data[i + 1] = string.pack(index_fmt, v.start + v.length); + end + local filename = getpath(username, host, datastore, "lidx"); + return atomic_store(filename, t_concat(data)); +end + +local index_mt = { + __index = function(t, i) + if type(i) ~= "number" or i % 1 ~= 0 or i < 0 then + return + end + if i <= 0 then + return 0 + end + local fh = t.file; + local pos = (i - 1) * index_item_size; + if fh:seek("set", pos) ~= pos then + return nil + end + local data = fh:read(index_item_size * 2); + if not data or #data ~= index_item_size * 2 then + return nil + end + local start, next_pos = string.unpack(index_fmt .. index_fmt, data); + if pos == 0 then + start = 0 + end + local length = next_pos - start; + local v = { start = start; length = length }; + t[i] = v; + return v; + end; + __len = function(t) + -- Account for both the header and the fence post error + return floor(t.file:seek("end") / index_item_size) - 1; + end; +} + +local function get_list_index(username, host, datastore) + log("debug", "Loading index for (%s@%s/%s)", username, host, datastore); + local index_filename = getpath(username, host, datastore, "lidx"); + local ih = io_open(index_filename); + if ih then + local magic = ih:read(#index_magic); + if magic ~= index_magic then + log("debug", "Index %q has wrong version number (got %q, expected %q), rebuilding...", index_filename, magic, index_magic); + -- wrong version or something + ih:close(); + ih = nil; + end + end + + if ih then + local first_length = string.unpack(index_fmt, ih:read(index_item_size)); + return setmetatable({ file = ih; { start = 0; length = first_length } }, index_mt); + end + + local index, err = build_list_index(username, host, datastore); + if not index then + return index, err + end + + -- TODO How to handle failure to store the index? + local dontcare = store_list_index(username, host, datastore, index); -- luacheck: ignore 211/dontcare + return index; +end + +local function list_load_one(fh, start, length) + if fh:seek("set", start) ~= start then + return nil + end + local raw_data = fh:read(length) + if not raw_data or #raw_data ~= length then + return + end + local item; + local data, err, errno = envload(raw_data, "@list", { + item = function(i) + item = i; + end; + }); + if not data then + return data, err, errno + end + local success, ret = pcall(data); + if not success then + return success, ret; + end + return item; +end + +local function list_close(list) + if list.index and list.index.file then + list.index.file:close(); + end + return list.file:close(); +end + +local indexed_list_mt = { + __index = function(t, i) + if type(i) ~= "number" or i % 1 ~= 0 or i < 1 then + return + end + local ix = t.index[i]; + if not ix then + return + end + local item = list_load_one(t.file, ix.start, ix.length); + return item; + end; + __len = function(t) + return #t.index; + end; + __close = list_close; +} + local function list_load(username, host, datastore) local items = {}; local data, err, errno = envloadfile(getpath(username, host, datastore, "list"), {item = function(i) t_insert(items, i); end}); @@ -314,6 +507,123 @@ return items; end +local function list_open(username, host, datastore) + if not index_magic then + log("debug", "Falling back from lazy loading to to loading full list for %s storage for user: %s@%s", datastore, username or "nil", host or "nil"); + return list_load(username, host, datastore); + end + local filename = getpath(username, host, datastore, "list"); + local file, err, errno = io_open(filename); + if not file then + if errno == ENOENT then + return nil; + end + return file, err, errno; + end + local index, err = get_list_index(username, host, datastore); + if not index then + file:close() + return index, err; + end + return setmetatable({ file = file; index = index; close = list_close }, indexed_list_mt); +end + +local function shift_index(index_filename, index, trim_to, offset) -- luacheck: ignore 212 + os_remove(index_filename); + return "deleted"; + -- TODO move and recalculate remaining items +end + +local function list_shift(username, host, datastore, trim_to) + if trim_to == 1 then + return true + end + if type(trim_to) ~= "number" or trim_to < 1 then + return nil, "invalid-argument"; + end + local list_filename = getpath(username, host, datastore, "list"); + local index_filename = getpath(username, host, datastore, "lidx"); + local index, err = get_list_index(username, host, datastore); + if not index then + return nil, err; + end + + local new_first = index[trim_to]; + if not new_first then + os_remove(index_filename); + return os_remove(list_filename); + end + + local offset = new_first.start; + if offset == 0 then + return true; + end + + --[[ + if remove_blocks then + local f, err = io_open(list_filename, "r+"); + if not f then + return f, err; + end + + local diff = 0; + local block_offset = 0; + if offset % 0x1000 ~= 0 then + -- Not an even block boundary, we will have to overwrite + diff = offset % 0x1000; + block_offset = offset - diff; + end + + if block_offset == 0 then + log("debug", "") + else + local ok, err = remove_blocks(f, 0, block_offset); + log("debug", "remove_blocks(%s, 0, %d)", f, block_offset); + if not ok then + log("warn", "Could not remove blocks from %q[%d, %d]: %s", list_filename, 0, block_offset, err); + else + if diff ~= 0 then + -- overwrite unaligned leftovers + if f:seek("set", 0) then + local wrote, err = f:write(string.rep("\n", diff)); + if not wrote then + log("error", "Could not blank out %q[%d, %d]: %s", list_filename, 0, diff, err); + end + end + end + local ok, err = f:close(); + shift_index(index_filename, index, trim_to, offset); -- Shift or delete the index + return ok, err; + end + end + end + --]] + + local r, err = io_open(list_filename, "r"); + if not r then + return nil, err; + end + local w, err = io_open(list_filename .. "~", "w"); + if not w then + return nil, err; + end + r:seek("set", offset); + for block in r:lines(0x1000) do + local ok, err = w:write(block); + if not ok then + return nil, err; + end + end + r:close(); + local ok, err = w:close(); + if not ok then + return nil, err; + end + shift_index(index_filename, index, trim_to, offset) + return os_rename(list_filename .. "~", list_filename); +end + + local type_map = { keyval = "dat"; list = "list"; @@ -392,6 +702,8 @@ local ok, err = do_remove(getpath(username, host, store_name, "list")); if not ok then errs[#errs+1] = err; end + local ok, err = do_remove(getpath(username, host, store_name, "lidx")); + if not ok then errs[#errs+1] = err; end end end return #errs == 0, t_concat(errs, ", "); @@ -414,4 +726,8 @@ purge = purge; path_decode = decode; path_encode = encode; + + build_list_index = build_list_index; + list_open = list_open; + list_shift = list_shift; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/datamapper.lua --- a/util/datamapper.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/datamapper.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,11 @@ -- This file is generated from teal-src/util/datamapper.lua -local st = require("util.stanza"); -local pointer = require("util.jsonpointer"); +if not math.type then + require("prosody.util.mathcompat") +end + +local st = require("prosody.util.stanza"); +local pointer = require("prosody.util.jsonpointer"); local schema_t = {} diff -r 54a936345aaa -r 5d9ec2e55d74 util/datetime.lua --- a/util/datetime.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/datetime.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,31 +12,41 @@ local os_date = os.date; local os_time = os.time; local os_difftime = os.difftime; +local floor = math.floor; local tonumber = tonumber; local _ENV = nil; -- luacheck: std none local function date(t) - return os_date("!%Y-%m-%d", t); + return os_date("!%Y-%m-%d", t and floor(t) or nil); end local function datetime(t) - return os_date("!%Y-%m-%dT%H:%M:%SZ", t); + if t == nil or t % 1 == 0 then + return os_date("!%Y-%m-%dT%H:%M:%SZ", t); + end + local m = t % 1; + local s = floor(t); + return os_date("!%Y-%m-%dT%H:%M:%S.%%06dZ", s):format(floor(m * 1000000)); end local function time(t) - return os_date("!%H:%M:%S", t); + if t == nil or t % 1 == 0 then + return os_date("!%H:%M:%S", t); + end + local m = t % 1; + local s = floor(t); + return os_date("!%H:%M:%S.%%06d", s):format(floor(m * 1000000)); end local function legacy(t) - return os_date("!%Y%m%dT%H:%M:%S", t); + return os_date("!%Y%m%dT%H:%M:%S", t and floor(t) or nil); end local function parse(s) if s then - local year, month, day, hour, min, sec, tzd; - year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$"); + local year, month, day, hour, min, sec, tzd = s:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d%.?%d*)([Z+%-]?.*)$"); if year then local now = os_time(); local time_offset = os_difftime(os_time(os_date("*t", now)), os_time(os_date("!*t", now))); -- to deal with local timezone @@ -49,8 +59,9 @@ tzd_offset = h * 60 * 60 + m * 60; if sign == "-" then tzd_offset = -tzd_offset; end end - sec = (sec + time_offset) - tzd_offset; - return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false}); + local prec = sec%1; + sec = floor(sec + time_offset) - tzd_offset; + return os_time({year=year, month=month, day=day, hour=hour, min=min, sec=sec, isdst=false})+prec; end end end diff -r 54a936345aaa -r 5d9ec2e55d74 util/dbuffer.lua --- a/util/dbuffer.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/dbuffer.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,4 @@ -local queue = require "util.queue"; +local queue = require "prosody.util.queue"; local s_byte, s_sub = string.byte, string.sub; local dbuffer_methods = {}; @@ -91,18 +91,18 @@ end function dbuffer_methods:discard(requested_bytes) - if requested_bytes > self._length then - return nil; + if self._length == 0 then return true; end + if not requested_bytes or requested_bytes >= self._length then + self.front_consumed = 0; + self._length = 0; + for _ in self.items:consume() do end + return true; end local chunk, read_bytes = self:read_chunk(requested_bytes); - if chunk then - requested_bytes = requested_bytes - read_bytes; - if requested_bytes == 0 then -- Already read everything we need - return true; - end - else - return nil; + requested_bytes = requested_bytes - read_bytes; + if requested_bytes == 0 then -- Already read everything we need + return true; end while chunk do diff -r 54a936345aaa -r 5d9ec2e55d74 util/debug.lua --- a/util/debug.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/debug.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,7 +12,7 @@ }; local optimal_line_length = 65; -local termcolours = require "util.termcolours"; +local termcolours = require "prosody.util.termcolours"; local getstring = termcolours.getstring; local styles; do diff -r 54a936345aaa -r 5d9ec2e55d74 util/dependencies.lua --- a/util/dependencies.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/dependencies.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,7 +7,6 @@ -- local function softreq(...) local ok, lib = pcall(require, ...); if ok then return lib; else return nil, lib; end end -local platform_table = require "util.human.io".table({ { width = 15, align = "right" }, { width = "100%" } }); -- Required to be able to find packages installed with luarocks if not softreq "luarocks.loader" then -- LuaRocks 2.x @@ -22,7 +21,7 @@ print("This package can be obtained in the following ways:"); print(""); for _, row in ipairs(sources) do - print(platform_table(row)); + print(string.format("%15s | %s", table.unpack(row))); end print(""); print(msg or (name.." is required for Prosody to run, so we will now exit.")); @@ -32,10 +31,10 @@ end local function check_dependencies() - if _VERSION < "Lua 5.1" then + if _VERSION < "Lua 5.2" then print "***********************************" print("Unsupported Lua version: ".._VERSION); - print("At least Lua 5.1 is required."); + print("At least Lua 5.2 is required."); print "***********************************" return false; end @@ -88,7 +87,7 @@ }, nil, err); end - local bit, err = softreq"util.bitcompat"; + local bit, err = softreq"prosody.util.bitcompat"; if not bit then missingdep("lua-bitops", { @@ -106,16 +105,16 @@ { "Source", "https://www.zash.se/luaunbound.html" }; }, "Old DNS resolver library will be used", err); else - package.preload["net.adns"] = function () - local ub = require "net.unbound"; + package.preload["prosody.net.adns"] = function () + local ub = require "prosody.net.unbound"; return ub; end end - local encodings, err = softreq "util.encodings" + local encodings, err = softreq "prosody.util.encodings" if not encodings then if err:match("module '[^']*' not found") then - missingdep("util.encodings", { + missingdep("prosody.util.encodings", { { "Windows", "Make sure you have encodings.dll from the Prosody distribution in util/" }; { "GNU/Linux", "Run './configure' and 'make' in the Prosody source directory to build util/encodings.so" }; }); @@ -130,10 +129,10 @@ fatal = true; end - local hashes, err = softreq "util.hashes" + local hashes, err = softreq "prosody.util.hashes" if not hashes then if err:match("module '[^']*' not found") then - missingdep("util.hashes", { + missingdep("prosody.util.hashes", { { "Windows", "Make sure you have hashes.dll from the Prosody distribution in util/" }; { "GNU/Linux", "Run './configure' and 'make' in the Prosody source directory to build util/hashes.so" }; }); @@ -155,7 +154,7 @@ if _VERSION > "Lua 5.4" then prosody.log("warn", "Support for %s is experimental, please report any issues", _VERSION); elseif _VERSION < "Lua 5.2" then - prosody.log("warn", "%s has several issues and support is being phased out, consider upgrading", _VERSION); + prosody.log("warn", "%s support is deprecated, upgrade as soon as possible", _VERSION); end local ssl = softreq"ssl"; if ssl then diff -r 54a936345aaa -r 5d9ec2e55d74 util/dns.lua --- a/util/dns.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/dns.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,9 +12,9 @@ local s_format = string.format; local s_sub = string.sub; -local iana_data = require "util.dnsregistry"; -local tohex = require "util.hex".encode; -local inet_ntop = require "util.net".ntop; +local iana_data = require "prosody.util.dnsregistry"; +local tohex = require "prosody.util.hex".encode; +local inet_ntop = require "prosody.util.net".ntop; -- Simplified versions of Waqas DNS parsers -- Only the per RR parsers are needed and only feed a single RR diff -r 54a936345aaa -r 5d9ec2e55d74 util/dnsregistry.lua --- a/util/dnsregistry.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/dnsregistry.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,5 +1,5 @@ -- Source: https://www.iana.org/assignments/dns-parameters/dns-parameters.xml --- Generated on 2022-02-02 +-- Generated on 2023-01-20 return { classes = { ["IN"] = 1; [1] = "IN"; @@ -61,7 +61,6 @@ ["NSEC3PARAM"] = 51; [51] = "NSEC3PARAM"; ["TLSA"] = 52; [52] = "TLSA"; ["SMIMEA"] = 53; [53] = "SMIMEA"; - ["Unassigned"] = 54; [54] = "Unassigned"; ["HIP"] = 55; [55] = "HIP"; ["NINFO"] = 56; [56] = "NINFO"; ["RKEY"] = 57; [57] = "RKEY"; diff -r 54a936345aaa -r 5d9ec2e55d74 util/envload.lua --- a/util/envload.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/envload.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,38 +6,19 @@ -- -- luacheck: ignore 113/setfenv 113/loadstring -local load, loadstring, setfenv = load, loadstring, setfenv; +local load = load; local io_open = io.open; -local envload; -local envloadfile; -if setfenv then - function envload(code, source, env) - local f, err = loadstring(code, source); - if f and env then setfenv(f, env); end - return f, err; - end +local function envload(code, source, env) + return load(code, source, nil, env); +end - function envloadfile(file, env) - local fh, err, errno = io_open(file); - if not fh then return fh, err, errno; end - local f, err = load(function () return fh:read(2048); end, "@"..file); - fh:close(); - if f and env then setfenv(f, env); end - return f, err; - end -else - function envload(code, source, env) - return load(code, source, nil, env); - end - - function envloadfile(file, env) - local fh, err, errno = io_open(file); - if not fh then return fh, err, errno; end - local f, err = load(fh:lines(2048), "@"..file, nil, env); - fh:close(); - return f, err; - end +local function envloadfile(file, env) + local fh, err, errno = io_open(file); + if not fh then return fh, err, errno; end + local f, err = load(fh:lines(2048), "@" .. file, nil, env); + fh:close(); + return f, err; end return { envload = envload, envloadfile = envloadfile }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/error.lua --- a/util/error.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/error.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,4 @@ -local id = require "util.id"; +local id = require "prosody.util.id"; local util_debug; -- only imported on-demand @@ -19,7 +19,7 @@ if opt.auto_inject_traceback ~= nil then auto_inject_traceback = opt.auto_inject_traceback; if auto_inject_traceback then - util_debug = require "util.debug"; + util_debug = require "prosody.util.debug"; end end end diff -r 54a936345aaa -r 5d9ec2e55d74 util/format.lua --- a/util/format.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/format.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,14 +6,20 @@ -- Provides some protection from e.g. CAPEC-135, CWE-117, CWE-134, CWE-93 local tostring = tostring; -local unpack = table.unpack or unpack; -- luacheck: ignore 113/unpack -local pack = require "util.table".pack; -- TODO table.pack in 5.2+ -local valid_utf8 = require "util.encodings".utf8.valid; +local unpack = table.unpack; +local pack = table.pack; +local valid_utf8 = require "prosody.util.encodings".utf8.valid; local type = type; -local dump = require "util.serialization".new("debug"); -local num_type = math.type or function (n) - return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; -end +local dump = require"prosody.util.serialization".new({ + preset = "compact"; + fallback = function(v, why) + return "_[[" .. (why or tostring(v)) .. "]] "; + end; + freeze = true; + fatal = false; + maxdepth = 5; +}); +local num_type = math.type; -- In Lua 5.3+ these formats throw an error if given a float local expects_integer = { c = true, d = true, i = true, o = true, u = true, X = true, x = true, }; @@ -35,7 +41,6 @@ ["\030"] = "\226\144\158", ["\031"] = "\226\144\159", ["\127"] = "\226\144\161", }; local supports_p = pcall(string.format, "%p", ""); -- >= Lua 5.4 -local supports_a = pcall(string.format, "%a", 0.0); -- > Lua 5.1 local function format(formatstring, ...) local args = pack(...); @@ -93,8 +98,6 @@ elseif expects_positive[option] and arg < 0 then args[i] = tostring(arg); return "[%s]"; - elseif (option == "a" or option == "A") and not supports_a then - return "%x"; else return -- acceptable number end diff -r 54a936345aaa -r 5d9ec2e55d74 util/fsm.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/fsm.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,154 @@ +local events = require "prosody.util.events"; + +local fsm_methods = {}; +local fsm_mt = { __index = fsm_methods }; + +local function is_fsm(o) + local mt = getmetatable(o); + return mt == fsm_mt; +end + +local function notify_transition(fire_event, transition_event) + local ret; + ret = fire_event("transition", transition_event); + if ret ~= nil then return ret; end + if transition_event.from ~= transition_event.to then + ret = fire_event("leave/"..transition_event.from, transition_event); + if ret ~= nil then return ret; end + end + ret = fire_event("transition/"..transition_event.name, transition_event); + if ret ~= nil then return ret; end +end + +local function notify_transitioned(fire_event, transition_event) + if transition_event.to ~= transition_event.from then + fire_event("enter/"..transition_event.to, transition_event); + end + if transition_event.name then + fire_event("transitioned/"..transition_event.name, transition_event); + end + fire_event("transitioned", transition_event); +end + +local function do_transition(name) + return function (self, attr) + local new_state = self.fsm.states[self.state][name] or self.fsm.states["*"][name]; + if not new_state then + return error(("Invalid state transition: %s cannot %s"):format(self.state, name)); + end + + local transition_event = { + instance = self; + + name = name; + to = new_state; + to_attr = attr; + + from = self.state; + from_attr = self.state_attr; + }; + + local fire_event = self.fsm.events.fire_event; + local ret = notify_transition(fire_event, transition_event); + if ret ~= nil then return nil, ret; end + + self.state = new_state; + self.state_attr = attr; + + notify_transitioned(fire_event, transition_event); + return true; + end; +end + +local function new(desc) + local self = setmetatable({ + default_state = desc.default_state; + events = events.new(); + }, fsm_mt); + + -- states[state_name][transition_name] = new_state_name + local states = { ["*"] = {} }; + if desc.default_state then + states[desc.default_state] = {}; + end + self.states = states; + + local instance_methods = {}; + self._instance_mt = { __index = instance_methods }; + + for _, transition in ipairs(desc.transitions or {}) do + local from_states = transition.from; + if type(from_states) ~= "table" then + from_states = { from_states }; + end + for _, from in ipairs(from_states) do + if not states[from] then + states[from] = {}; + end + if not states[transition.to] then + states[transition.to] = {}; + end + if states[from][transition.name] then + return error(("Duplicate transition in FSM specification: %s from %s"):format(transition.name, from)); + end + states[from][transition.name] = transition.to; + end + + -- Add public method to trigger this transition + instance_methods[transition.name] = do_transition(transition.name); + end + + if desc.state_handlers then + for state_name, handler in pairs(desc.state_handlers) do + self.events.add_handler("enter/"..state_name, handler); + end + end + + if desc.transition_handlers then + for transition_name, handler in pairs(desc.transition_handlers) do + self.events.add_handler("transition/"..transition_name, handler); + end + end + + if desc.handlers then + self.events.add_handlers(desc.handlers); + end + + return self; +end + +function fsm_methods:init(state_name, state_attr) + local initial_state = assert(state_name or self.default_state, "no initial state specified"); + if not self.states[initial_state] then + return error("Invalid initial state: "..initial_state); + end + local instance = setmetatable({ + fsm = self; + state = initial_state; + state_attr = state_attr; + }, self._instance_mt); + + if initial_state ~= self.default_state then + local fire_event = self.events.fire_event; + notify_transitioned(fire_event, { + instance = instance; + + to = initial_state; + to_attr = state_attr; + + from = self.default_state; + }); + end + + return instance; +end + +function fsm_methods:is_instance(o) + local mt = getmetatable(o); + return mt == self._instance_mt; +end + +return { + new = new; + is_fsm = is_fsm; +}; diff -r 54a936345aaa -r 5d9ec2e55d74 util/gc.lua --- a/util/gc.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/gc.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,4 @@ -local set = require "util.set"; +local set = require "prosody.util.set"; local known_options = { incremental = set.new { "mode", "threshold", "speed", "step_size" }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/hashring.lua --- a/util/hashring.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/hashring.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,3 +1,5 @@ +local it = require "prosody.util.iterators"; + local function generate_ring(nodes, num_replicas, hash) local new_ring = {}; for _, node_name in ipairs(nodes) do @@ -28,18 +30,22 @@ return setmetatable({ nodes = {}, num_replicas = num_replicas, hash = hash_function }, hashring_mt); end; -function hashring_methods:add_node(name) +function hashring_methods:add_node(name, value) self.ring = nil; - self.nodes[name] = true; + self.nodes[name] = value == nil and true or value; table.insert(self.nodes, name); return true; end function hashring_methods:add_nodes(nodes) self.ring = nil; - for _, node_name in ipairs(nodes) do - if not self.nodes[node_name] then - self.nodes[node_name] = true; + local iter = pairs; + if nodes[1] then -- simple array? + iter = it.values; + end + for node_name, node_value in iter(nodes) do + if self.nodes[node_name] == nil then + self.nodes[node_name] = node_value == nil and true or node_value; table.insert(self.nodes, node_name); end end @@ -48,7 +54,7 @@ function hashring_methods:remove_node(node_name) self.ring = nil; - if self.nodes[node_name] then + if self.nodes[node_name] ~= nil then for i, stored_node_name in ipairs(self.nodes) do if node_name == stored_node_name then self.nodes[node_name] = nil; @@ -69,18 +75,26 @@ function hashring_methods:clone() local clone_hashring = new(self.num_replicas, self.hash); - clone_hashring:add_nodes(self.nodes); + for node_name, node_value in pairs(self.nodes) do + clone_hashring.nodes[node_name] = node_value; + end + clone_hashring.ring = nil; return clone_hashring; end function hashring_methods:get_node(key) + local node; local key_hash = self.hash(key); for _, replica_hash in ipairs(self.ring) do if key_hash < replica_hash then - return self.ring[replica_hash]; + node = self.ring[replica_hash]; + break; end end - return self.ring[self.ring[1]]; + if not node then + node = self.ring[self.ring[1]]; + end + return node, self.nodes[node]; end return { diff -r 54a936345aaa -r 5d9ec2e55d74 util/helpers.lua --- a/util/helpers.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/helpers.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,11 +6,11 @@ -- COPYING file in the source package for more information. -- -local debug = require "util.debug"; +local debug = require "prosody.util.debug"; -- Helper functions for debugging -local log = require "util.logger".init("util.debug"); +local log = require "prosody.util.logger".init("util.debug"); local function log_events(events, name, logger) local f = events.fire_event; diff -r 54a936345aaa -r 5d9ec2e55d74 util/hmac.lua --- a/util/hmac.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/hmac.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,11 +8,15 @@ -- COMPAT: Only for external pre-0.9 modules -local hashes = require "util.hashes" +local hashes = require "prosody.util.hashes" return { md5 = hashes.hmac_md5, sha1 = hashes.hmac_sha1, + sha224 = hashes.hmac_sha224, sha256 = hashes.hmac_sha256, + sha384 = hashes.hmac_sha384, sha512 = hashes.hmac_sha512, + blake2s256 = hashes.hmac_blake2s256, + blake2b512 = hashes.hmac_blake2b512, }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/http.lua --- a/util/http.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/http.lua Wed Mar 27 15:39:03 2024 +0000 @@ -69,9 +69,42 @@ return path; end +--- Parse the RFC 7239 Forwarded header into array of key-value pairs. +local function parse_forwarded(forwarded) + if type(forwarded) ~= "string" then + return nil; + end + + local fwd = {}; -- array + local cur = {}; -- map, to which we add the next key-value pair + for key, quoted, value, delim in forwarded:gmatch("(%w+)%s*=%s*(\"?)([^,;\"]+)%2%s*(.?)") do + -- FIXME quoted quotes like "foo\"bar" + -- unlikely when only dealing with IP addresses + if quoted == '"' then + value = value:gsub("\\(.)", "%1"); + end + + cur[key:lower()] = value; + if delim == "" or delim == "," then + t_insert(fwd, cur) + if delim == "" then + -- end of the string + break; + end + cur = {}; + elseif delim ~= ";" then + -- misparsed + return false; + end + end + + return fwd; +end + return { urlencode = urlencode, urldecode = urldecode; formencode = formencode, formdecode = formdecode; contains_token = contains_token; normalize_path = normalize_path; + parse_forwarded = parse_forwarded; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/human/io.lua --- a/util/human/io.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/human/io.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,5 +1,6 @@ -local array = require "util.array"; -local utf8 = rawget(_G, "utf8") or require"util.encodings".utf8; +local array = require "prosody.util.array"; +local pposix = require "prosody.util.pposix"; +local utf8 = rawget(_G, "utf8") or require"prosody.util.encodings".utf8; local len = utf8.len or function(s) local _, count = s:gsub("[%z\001-\127\194-\253][\128-\191]*", ""); return count; @@ -8,7 +9,7 @@ local function getchar(n) local stty_ret = os.execute("stty raw -echo 2>/dev/null"); local ok, char; - if stty_ret == true or stty_ret == 0 then + if stty_ret then ok, char = pcall(io.read, n or 1); os.execute("stty sane"); else @@ -30,15 +31,12 @@ end local function getpass() - local stty_ret, _, status_code = os.execute("stty -echo 2>/dev/null"); - if status_code then -- COMPAT w/ Lua 5.1 - stty_ret = status_code; - end - if stty_ret ~= 0 then + local stty_ret = os.execute("stty -echo 2>/dev/null"); + if not stty_ret then io.write("\027[08m"); -- ANSI 'hidden' text attribute end local ok, pass = pcall(io.read, "*l"); - if stty_ret == 0 then + if stty_ret then os.execute("stty sane"); else io.write("\027[00m"); @@ -111,14 +109,30 @@ end end +local function term_width(default) + local env_cols = tonumber(os.getenv "COLUMNS"); + if env_cols then return env_cols; end + if not pposix.isatty(io.stdout) then + return default; + end + local stty = io.popen("stty -a"); + if not stty then return default; end + local result = stty:read("*a"); + if result then + result = result:match("%f[%w]columns[ =]*(%d+)"); + end + stty:close(); + return tonumber(result or default); +end + local function ellipsis(s, width) if len(s) <= width then return s; end - if width == 1 then return "…"; end + if width <= 1 then return "…"; end return utf8_cut(s, width - 1) .. "…"; end local function new_table(col_specs, max_width) - max_width = max_width or tonumber(os.getenv("COLUMNS")) or 80; + max_width = max_width or term_width(80); local separator = " | "; local widths = {}; @@ -127,21 +141,28 @@ -- Calculate width of fixed-size columns for i = 1, #col_specs do local width = col_specs[i].width or "0"; - if not(type(width) == "string" and width:sub(-1) == "%") then + if not (type(width) == "string" and width:match("[p%%]$")) then local title = col_specs[i].title; width = math.max(tonumber(width), title and (#title+1) or 0); widths[i] = width; free_width = free_width - width; - if i > 1 then - free_width = free_width - #separator; - end end end - -- Calculate width of %-based columns + + -- Calculate width of proportional columns + local total_proportional_width = 0; for i = 1, #col_specs do if not widths[i] then - local pc_width = tonumber((col_specs[i].width:gsub("%%$", ""))); - widths[i] = math.floor(free_width*(pc_width/100)); + local width_spec = col_specs[i].width:match("([%d%.]+)[p%%]"); + total_proportional_width = total_proportional_width + tonumber(width_spec); + end + end + + for i = 1, #col_specs do + if not widths[i] then + local width_spec = col_specs[i].width:match("([%d%.]+)[p%%]"); + local rel_width = tonumber(width_spec); + widths[i] = math.floor(free_width*(rel_width/total_proportional_width)); end end @@ -155,7 +176,7 @@ local width = widths[i]; local v = row[not titles and column.key or i]; if not titles and column.mapper then - v = column.mapper(v, row); + v = column.mapper(v, row, width, column); end if v == nil then v = column.default or ""; @@ -169,12 +190,36 @@ v = padright(v, width); end elseif len(v) > width then - v = ellipsis(v, width); + v = (column.ellipsis or ellipsis)(v, width); end table.insert(output, v); end return table.concat(output, separator); - end; + end, max_width; +end + +local day = 86400; +local multipliers = { + d = day, w = day * 7, mon = 31 * day, y = 365.2425 * day; + s = 1, min = 60, h = 3600, ho = 3600 +}; + +local function parse_duration(duration_string) + local n, m = duration_string:lower():match("(%d+)%s*([smhdwy]?[io]?n?)"); + if not n or not multipliers[m] then return nil; end + return tonumber(n) * ( multipliers[m] or 1 ); +end + +local multipliers_lax = setmetatable({ + m = multipliers.mon; + mo = multipliers.mon; + mi = multipliers.min; +}, { __index = multipliers }); + +local function parse_duration_lax(duration_string) + local n, m = duration_string:lower():match("(%d+)%s*([smhdwy]?[io]?)"); + if not n then return nil; end + return tonumber(n) * ( multipliers_lax[m] or 1 ); end return { @@ -187,6 +232,9 @@ printf = printf; padleft = padleft; padright = padright; + term_width = term_width; ellipsis = ellipsis; table = new_table; + parse_duration = parse_duration; + parse_duration_lax = parse_duration_lax; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/human/units.lua --- a/util/human/units.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/human/units.lua Wed Mar 27 15:39:03 2024 +0000 @@ -4,15 +4,7 @@ local math_log = math.log; local math_max = math.max; local math_min = math.min; -local unpack = table.unpack or unpack; --luacheck: ignore 113 - -if math_log(10, 10) ~= 1 then - -- Lua 5.1 COMPAT - local log10 = math.log10; - function math_log(n, base) - return log10(n) / log10(base); - end -end +local unpack = table.unpack; local large = { "k", 1000, diff -r 54a936345aaa -r 5d9ec2e55d74 util/id.lua --- a/util/id.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/id.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,8 +8,8 @@ -- local s_gsub = string.gsub; -local random_bytes = require "util.random".bytes; -local base64_encode = require "util.encodings".base64.encode; +local random_bytes = require "prosody.util.random".bytes; +local base64_encode = require "prosody.util.encodings".base64.encode; local b64url = { ["+"] = "-", ["/"] = "_", ["="] = "" }; local function b64url_random(len) diff -r 54a936345aaa -r 5d9ec2e55d74 util/import.lua --- a/util/import.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/import.lua Wed Mar 27 15:39:03 2024 +0000 @@ -8,7 +8,7 @@ -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local t_insert = table.insert; function _G.import(module, ...) local m = package.loaded[module] or require(module); diff -r 54a936345aaa -r 5d9ec2e55d74 util/ip.lua --- a/util/ip.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/ip.lua Wed Mar 27 15:39:03 2024 +0000 @@ -5,8 +5,8 @@ -- COPYING file in the source package for more information. -- -local net = require "util.net"; -local hex = require "util.hex"; +local net = require "prosody.util.net"; +local strbit = require "prosody.util.strbitop"; local ip_methods = {}; @@ -28,13 +28,6 @@ return ipA.packed == ipB.packed; end -local hex2bits = { - ["0"] = "0000", ["1"] = "0001", ["2"] = "0010", ["3"] = "0011", - ["4"] = "0100", ["5"] = "0101", ["6"] = "0110", ["7"] = "0111", - ["8"] = "1000", ["9"] = "1001", ["A"] = "1010", ["B"] = "1011", - ["C"] = "1100", ["D"] = "1101", ["E"] = "1110", ["F"] = "1111", -}; - local function new_ip(ipStr, proto) local zone; if (not proto or proto == "IPv6") and ipStr:find('%', 1, true) then @@ -66,27 +59,18 @@ return net.ntop(self.packed); end -function ip_methods.bits(ip) - return hex.encode(ip.packed):upper():gsub(".", hex2bits); -end - -function ip_methods.bits_full(ip) +-- Returns the longest packed representation, i.e. IPv4 will be mapped +function ip_methods.packed_full(ip) if ip.proto == "IPv4" then ip = ip.toV4mapped; end - return ip.bits; + return ip.packed; end local match; local function commonPrefixLength(ipA, ipB) - ipA, ipB = ipA.bits_full, ipB.bits_full; - for i = 1, 128 do - if ipA:sub(i,i) ~= ipB:sub(i,i) then - return i-1; - end - end - return 128; + return strbit.common_prefix_bits(ipA.packed_full, ipB.packed_full); end -- Instantiate once @@ -238,7 +222,22 @@ bits = bits + (128 - 32); end end - return ipA.bits:sub(1, bits) == ipB.bits:sub(1, bits); + return strbit.common_prefix_bits(ipA.packed, ipB.packed) >= bits; +end + +local function is_ip(obj) + return getmetatable(obj) == ip_mt; +end + +local function truncate(ip, n_bits) + if n_bits % 8 ~= 0 then + return error("ip.truncate() only supports multiples of 8 bits"); + end + local n_octets = n_bits / 8; + if not is_ip(ip) then + ip = new_ip(ip); + end + return new_ip(net.ntop(ip.packed:sub(1, n_octets)..("\0"):rep(#ip.packed-n_octets))) end return { @@ -246,4 +245,6 @@ commonPrefixLength = commonPrefixLength, parse_cidr = parse_cidr, match = match, + is_ip = is_ip; + truncate = truncate; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/iterators.lua --- a/util/iterators.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/iterators.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,8 +12,8 @@ local t_insert = table.insert; local next = next; -local unpack = table.unpack or unpack; --luacheck: ignore 113 -local pack = table.pack or require "util.table".pack; +local unpack = table.unpack; +local pack = table.pack; local type = type; local table, setmetatable = table, setmetatable; @@ -240,7 +240,8 @@ end function it.join(f, s, var) - return setmetatable({ {f, s, var} }, join_mt); + local t = setmetatable({ {f, s, var} }, join_mt); + return t, { t, 1 }; end return it; diff -r 54a936345aaa -r 5d9ec2e55d74 util/jid.lua --- a/util/jid.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/jid.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,9 +10,9 @@ local select = select; local match, sub = string.match, string.sub; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local nameprep = require "util.encodings".stringprep.nameprep; -local resourceprep = require "util.encodings".stringprep.resourceprep; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local nameprep = require "prosody.util.encodings".stringprep.nameprep; +local resourceprep = require "prosody.util.encodings".stringprep.resourceprep; local escapes = { [" "] = "\\20"; ['"'] = "\\22"; @@ -35,8 +35,7 @@ if jid == nil then return; end local node, nodepos = match(jid, "^([^@/]+)@()"); local host, hostpos = match(jid, "^([^@/]+)()", nodepos); - if node ~= nil and host == nil then return nil, nil, nil; end - local resource = match(jid, "^/(.+)$", hostpos); + local resource = host and match(jid, "^/(.+)$", hostpos); if (host == nil) or ((resource == nil) and #jid >= hostpos) then return nil, nil, nil; end return node, host, resource; end @@ -91,9 +90,9 @@ -- TODO compare to table of rules? local jid_node, jid_host, jid_resource = split(jid); local acl_node, acl_host, acl_resource = split(acl); - if ((acl_node ~= nil and acl_node == jid_node) or acl_node == nil) and - ((acl_host ~= nil and acl_host == jid_host) or acl_host == nil) and - ((acl_resource ~= nil and acl_resource == jid_resource) or acl_resource == nil) then + if (acl_node == nil or acl_node == jid_node) and + (acl_host == nil or acl_host == jid_host) and + (acl_resource == nil or acl_resource == jid_resource) then return true end return false @@ -111,6 +110,7 @@ return (select(3, split(jid))); end +-- TODO Forbid \20 at start and end of escaped output per XEP-0106 v1.1 local function escape(s) return s and (s:gsub("\\%x%x", backslash_escapes):gsub("[\"&'/:<>@ ]", escapes)); end local function unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end diff -r 54a936345aaa -r 5d9ec2e55d74 util/json.lua --- a/util/json.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/json.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,12 +10,12 @@ local t_insert, t_concat, t_remove = table.insert, table.concat, table.remove; local s_char = string.char; local tostring, tonumber = tostring, tonumber; -local pairs, ipairs, spairs = pairs, ipairs, require "util.iterators".sorted_pairs; +local pairs, ipairs, spairs = pairs, ipairs, require "prosody.util.iterators".sorted_pairs; local next = next; local getmetatable, setmetatable = getmetatable, setmetatable; local print = print; -local has_array, array = pcall(require, "util.array"); +local has_array, array = pcall(require, "prosody.util.array"); local array_mt = has_array and getmetatable(array()) or {}; --module("json") diff -r 54a936345aaa -r 5d9ec2e55d74 util/jsonpointer.lua --- a/util/jsonpointer.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/jsonpointer.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,4 @@ -local m_type = math.type or function (n) - return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; -end; +local m_type = math.type; local function unescape_token(escaped_token) local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~") diff -r 54a936345aaa -r 5d9ec2e55d74 util/jsonschema.lua --- a/util/jsonschema.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/jsonschema.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,16 +1,19 @@ -- This file is generated from teal-src/util/jsonschema.lua -local m_type = function(n) - return type(n) == "number" and n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; +if not math.type then + require("prosody.util.mathcompat") +end + +local utf8_enc = rawget(_G, "utf8") or require("prosody.util.encodings").utf8; +local utf8_len = utf8_enc.len or function(s) + local _, count = s:gsub("[%z\001-\127\194-\253][\128-\191]*", ""); + return count end; -local json = require("util.json") + +local json = require("prosody.util.json") local null = json.null; -local pointer = require("util.jsonpointer") - -local json_type_name = json.json_type_name - -local schema_t = {} +local pointer = require("prosody.util.jsonpointer") local json_schema_object = { xml_t = {} } @@ -22,7 +25,7 @@ elseif schema == "array" and type(data) == "table" then return type(data) == "table" and (next(data) == nil or type((next(data, nil))) == "number") elseif schema == "integer" then - return m_type(data) == schema + return math.type(data) == schema elseif schema == "null" then return data == null elseif type(schema) == "table" then @@ -37,33 +40,35 @@ end end -local complex_validate +local function mkerr(sloc, iloc, err) + return { schemaLocation = sloc; instanceLocation = iloc; error = err } +end -local function validate(schema, data, root) +local function validate(schema, data, root, sloc, iloc, errs) if type(schema) == "boolean" then return schema - else - return complex_validate(schema, data, root) end -end - -function complex_validate(schema, data, root) if root == nil then root = schema + iloc = "" + sloc = "" + errs = {}; end if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then local referenced = pointer.resolve(root, schema["$ref"]:sub(2)) if referenced ~= nil and referenced ~= root and referenced ~= schema then - if not validate(referenced, data, root) then - return false + if not validate(referenced, data, root, schema["$ref"], iloc, errs) then + table.insert(errs, mkerr(sloc .. "/$ref", iloc, "Subschema failed validation")) + return false, errs end end end if not simple_validate(schema.type, data) then - return false + table.insert(errs, mkerr(sloc .. "/type", iloc, "unexpected type")); + return false, errs end if schema.type == "object" then @@ -71,7 +76,8 @@ for k in pairs(data) do if not (type(k) == "string") then - return false + table.insert(errs, mkerr(sloc .. "/type", iloc, "'object' had non-string keys")); + return false, errs end end end @@ -81,8 +87,9 @@ if type(data) == "table" then for i in pairs(data) do - if not (m_type(i) == "integer") then - return false + if not (math.type(i) == "integer") then + table.insert(errs, mkerr(sloc .. "/type", iloc, "'array' had non-integer keys")); + return false, errs end end end @@ -98,146 +105,217 @@ end end if not match then - return false + table.insert(errs, mkerr(sloc .. "/enum", iloc, "not one of the enumerated values")); + return false, errs end end if type(data) == "string" then - if schema.maxLength and #data > schema.maxLength then - return false + if schema.maxLength and utf8_len(data) > schema.maxLength then + table.insert(errs, mkerr(sloc .. "/maxLength", iloc, "string too long")) + return false, errs end - if schema.minLength and #data < schema.minLength then - return false + if schema.minLength and utf8_len(data) < schema.minLength then + table.insert(errs, mkerr(sloc .. "/maxLength", iloc, "string too short")) + return false, errs + end + if schema.luaPattern and not data:match(schema.luaPattern) then + table.insert(errs, mkerr(sloc .. "/luaPattern", iloc, "string does not match pattern")) + return false, errs end end if type(data) == "number" then if schema.multipleOf and (data == 0 or data % schema.multipleOf ~= 0) then - return false + table.insert(errs, mkerr(sloc .. "/luaPattern", iloc, "not a multiple")) + return false, errs end if schema.maximum and not (data <= schema.maximum) then - return false + table.insert(errs, mkerr(sloc .. "/maximum", iloc, "number exceeds maximum")) + return false, errs end if schema.exclusiveMaximum and not (data < schema.exclusiveMaximum) then - return false + table.insert(errs, mkerr(sloc .. "/exclusiveMaximum", iloc, "number exceeds exclusive maximum")) + return false, errs end if schema.minimum and not (data >= schema.minimum) then - return false + table.insert(errs, mkerr(sloc .. "/minimum", iloc, "number below minimum")) + return false, errs end if schema.exclusiveMinimum and not (data > schema.exclusiveMinimum) then - return false + table.insert(errs, mkerr(sloc .. "/exclusiveMinimum", iloc, "number below exclusive minimum")) + return false, errs end end if schema.allOf then - for _, sub in ipairs(schema.allOf) do - if not validate(sub, data, root) then - return false + for i, sub in ipairs(schema.allOf) do + if not validate(sub, data, root, sloc .. "/allOf/" .. i, iloc, errs) then + table.insert(errs, mkerr(sloc .. "/allOf", iloc, "did not match all subschemas")) + return false, errs end end end if schema.oneOf then local valid = 0 - for _, sub in ipairs(schema.oneOf) do - if validate(sub, data, root) then + for i, sub in ipairs(schema.oneOf) do + if validate(sub, data, root, sloc .. "/oneOf" .. i, iloc, errs) then valid = valid + 1 end end if valid ~= 1 then - return false + table.insert(errs, mkerr(sloc .. "/oneOf", iloc, "did not match exactly one subschema")) + return false, errs end end if schema.anyOf then local match = false - for _, sub in ipairs(schema.anyOf) do - if validate(sub, data, root) then + for i, sub in ipairs(schema.anyOf) do + if validate(sub, data, root, sloc .. "/anyOf/" .. i, iloc, errs) then match = true break end end if not match then - return false + table.insert(errs, mkerr(sloc .. "/anyOf", iloc, "did not match any subschema")) + return false, errs end end if schema["not"] then - if validate(schema["not"], data, root) then - return false + if validate(schema["not"], data, root, sloc .. "/not", iloc, errs) then + table.insert(errs, mkerr(sloc .. "/not", iloc, "did match subschema")) + return false, errs end end if schema["if"] ~= nil then - if validate(schema["if"], data, root) then + if validate(schema["if"], data, root, sloc .. "/if", iloc, errs) then if schema["then"] then - return validate(schema["then"], data, root) + if not validate(schema["then"], data, root, sloc .. "/then", iloc, errs) then + table.insert(errs, mkerr(sloc .. "/then", iloc, "did not match subschema")) + return false, errs + end end else if schema["else"] then - return validate(schema["else"], data, root) + if not validate(schema["else"], data, root, sloc .. "/else", iloc, errs) then + table.insert(errs, mkerr(sloc .. "/else", iloc, "did not match subschema")) + return false, errs + end end end end if schema.const ~= nil and schema.const ~= data then - return false + table.insert(errs, mkerr(sloc .. "/const", iloc, "did not match constant value")) + return false, errs end if type(data) == "table" then - if schema.maxItems and #data > schema.maxItems then - return false + if schema.maxItems and #(data) > schema.maxItems then + table.insert(errs, mkerr(sloc .. "/maxItems", iloc, "too many items")) + return false, errs end - if schema.minItems and #data < schema.minItems then - return false + if schema.minItems and #(data) < schema.minItems then + table.insert(errs, mkerr(sloc .. "/minItems", iloc, "too few items")) + return false, errs end if schema.required then for _, k in ipairs(schema.required) do if data[k] == nil then - return false + table.insert(errs, mkerr(sloc .. "/required", iloc .. "/" .. tostring(k), "missing required property")) + return false, errs + end + end + end + + if schema.dependentRequired then + for k, reqs in pairs(schema.dependentRequired) do + if data[k] ~= nil then + for _, req in ipairs(reqs) do + if data[req] == nil then + table.insert(errs, mkerr(sloc .. "/dependentRequired", iloc, "missing dependent required property")) + return false, errs + end + end end end end if schema.propertyNames ~= nil then + for k in pairs(data) do - if not validate(schema.propertyNames, k, root) then - return false + if not validate(schema.propertyNames, k, root, sloc .. "/propertyNames", iloc .. "/" .. tostring(k), errs) then + table.insert(errs, mkerr(sloc .. "/propertyNames", iloc .. "/" .. tostring(k), "a property name did not match subschema")) + return false, errs end end end + local seen_properties = {} + if schema.properties then for k, sub in pairs(schema.properties) do - if data[k] ~= nil and not validate(sub, data[k], root) then - return false + if data[k] ~= nil and not validate(sub, data[k], root, sloc .. "/" .. tostring(k), iloc .. "/" .. tostring(k), errs) then + table.insert(errs, mkerr(sloc .. "/" .. tostring(k), iloc .. "/" .. tostring(k), "a property did not match subschema")) + return false, errs + end + seen_properties[k] = true + end + end + + if schema.luaPatternProperties then + + for pattern, sub in pairs(schema.luaPatternProperties) do + for k in pairs(data) do + if type(k) == "string" and k:match(pattern) then + if not validate(sub, data[k], root, sloc .. "/luaPatternProperties", iloc, errs) then + table.insert(errs, mkerr(sloc .. "/luaPatternProperties/" .. pattern, iloc .. "/" .. tostring(k), "a property did not match subschema")) + return false, errs + end + seen_properties[k] = true + end end end end if schema.additionalProperties ~= nil then for k, v in pairs(data) do - if schema.properties == nil or schema.properties[k] == nil then - if not validate(schema.additionalProperties, v, root) then - return false + if not seen_properties[k] then + if not validate(schema.additionalProperties, v, root, sloc .. "/additionalProperties", iloc .. "/" .. tostring(k), errs) then + table.insert(errs, mkerr(sloc .. "/additionalProperties", iloc .. "/" .. tostring(k), "additional property did not match subschema")) + return false, errs end end end end + if schema.dependentSchemas then + for k, sub in pairs(schema.dependentSchemas) do + if data[k] ~= nil and not validate(sub, data, root, sloc .. "/dependentSchemas/" .. k, iloc, errs) then + table.insert(errs, mkerr(sloc .. "/dependentSchemas", iloc .. "/" .. tostring(k), "did not match dependent subschema")) + return false, errs + end + end + end + if schema.uniqueItems then local values = {} for _, v in pairs(data) do if values[v] then - return false + table.insert(errs, mkerr(sloc .. "/uniqueItems", iloc, "had duplicate items")) + return false, errs end values[v] = true end @@ -248,32 +326,39 @@ for i, s in ipairs(schema.prefixItems) do if data[i] == nil then break - elseif validate(s, data[i], root) then + elseif validate(s, data[i], root, sloc .. "/prefixItems/" .. i, iloc .. "/" .. i, errs) then p = i else - return false + table.insert(errs, mkerr(sloc .. "/prefixItems/" .. i, iloc .. "/" .. tostring(i), "did not match subschema")) + return false, errs end end end if schema.items ~= nil then - for i = p + 1, #data do - if not validate(schema.items, data[i], root) then - return false + for i = p + 1, #(data) do + if not validate(schema.items, data[i], root, sloc, iloc .. "/" .. i, errs) then + table.insert(errs, mkerr(sloc .. "/prefixItems/" .. i, iloc .. "/" .. i, "did not match subschema")) + return false, errs end end end if schema.contains ~= nil then - local found = false - for i = 1, #data do - if validate(schema.contains, data[i], root) then - found = true - break + local found = 0 + for i = 1, #(data) do + if validate(schema.contains, data[i], root, sloc .. "/contains", iloc .. "/" .. i, errs) then + found = found + 1 + else + table.insert(errs, mkerr(sloc .. "/contains", iloc .. "/" .. i, "did not match subschema")) end end - if not found then - return false + if found < (schema.minContains or 1) then + table.insert(errs, mkerr(sloc .. "/minContains", iloc, "too few matches")) + return false, errs + elseif found > (schema.maxContains or math.huge) then + table.insert(errs, mkerr(sloc .. "/maxContains", iloc, "too many matches")) + return false, errs end end end diff -r 54a936345aaa -r 5d9ec2e55d74 util/jwt.lua --- a/util/jwt.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/jwt.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,9 +1,10 @@ local s_gsub = string.gsub; -local json = require "util.json"; -local hashes = require "util.hashes"; -local base64_encode = require "util.encodings".base64.encode; -local base64_decode = require "util.encodings".base64.decode; -local secure_equals = require "util.hashes".equals; +local crypto = require "prosody.util.crypto"; +local json = require "prosody.util.json"; +local hashes = require "prosody.util.hashes"; +local base64_encode = require "prosody.util.encodings".base64.encode; +local base64_decode = require "prosody.util.encodings".base64.decode; +local secure_equals = require "prosody.util.hashes".equals; local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" }; local function b64url(data) @@ -13,17 +14,8 @@ return base64_decode(s_gsub(data, "[-_]", b64url_rep).."=="); end -local static_header = b64url('{"alg":"HS256","typ":"JWT"}') .. '.'; - -local function sign(key, payload) - local encoded_payload = json.encode(payload); - local signed = static_header .. b64url(encoded_payload); - local signature = hashes.hmac_sha256(key, signed); - return signed .. "." .. b64url(signature); -end - local jwt_pattern = "^(([A-Za-z0-9-_]+)%.([A-Za-z0-9-_]+))%.([A-Za-z0-9-_]+)$" -local function verify(key, blob) +local function decode_jwt(blob, expected_alg) local signed, bheader, bpayload, signature = string.match(blob, jwt_pattern); if not signed then return nil, "invalid-encoding"; @@ -31,21 +23,197 @@ local header = json.decode(unb64url(bheader)); if not header or type(header) ~= "table" then return nil, "invalid-header"; - elseif header.alg ~= "HS256" then + elseif header.alg ~= expected_alg then return nil, "unsupported-algorithm"; end - if not secure_equals(b64url(hashes.hmac_sha256(key, signed)), signature) then - return false, "signature-mismatch"; - end - local payload, err = json.decode(unb64url(bpayload)); + return signed, signature, bpayload; +end + +local function new_static_header(algorithm_name) + return b64url('{"alg":"'..algorithm_name..'","typ":"JWT"}') .. '.'; +end + +local function decode_raw_payload(raw_payload) + local payload, err = json.decode(unb64url(raw_payload)); if err ~= nil then return nil, "json-decode-error"; + elseif type(payload) ~= "table" then + return nil, "invalid-payload-type"; end return true, payload; end -return { - sign = sign; - verify = verify; +-- HS*** family +local function new_hmac_algorithm(name) + local static_header = new_static_header(name); + + local hmac = hashes["hmac_sha"..name:sub(-3)]; + + local function sign(key, payload) + local encoded_payload = json.encode(payload); + local signed = static_header .. b64url(encoded_payload); + local signature = hmac(key, signed); + return signed .. "." .. b64url(signature); + end + + local function verify(key, blob) + local signed, signature, raw_payload = decode_jwt(blob, name); + if not signed then return nil, signature; end -- nil, err + + if not secure_equals(b64url(hmac(key, signed)), signature) then + return false, "signature-mismatch"; + end + + return decode_raw_payload(raw_payload); + end + + local function load_key(key) + assert(type(key) == "string", "key must be string (long, random, secure)"); + return key; + end + + return { sign = sign, verify = verify, load_key = load_key }; +end + +local function new_crypto_algorithm(name, key_type, c_sign, c_verify, sig_encode, sig_decode) + local static_header = new_static_header(name); + + return { + sign = function (private_key, payload) + local encoded_payload = json.encode(payload); + local signed = static_header .. b64url(encoded_payload); + + local signature = c_sign(private_key, signed); + if sig_encode then + signature = sig_encode(signature); + end + + return signed.."."..b64url(signature); + end; + + verify = function (public_key, blob) + local signed, signature, raw_payload = decode_jwt(blob, name); + if not signed then return nil, signature; end -- nil, err + + signature = unb64url(signature); + if sig_decode and signature then + signature = sig_decode(signature); + end + if not signature then + return false, "signature-mismatch"; + end + + local verify_ok = c_verify(public_key, signed, signature); + if not verify_ok then + return false, "signature-mismatch"; + end + + return decode_raw_payload(raw_payload); + end; + + load_public_key = function (public_key_pem) + local key = assert(crypto.import_public_pem(public_key_pem)); + assert(key:get_type() == key_type, "incorrect key type"); + return key; + end; + + load_private_key = function (private_key_pem) + local key = assert(crypto.import_private_pem(private_key_pem)); + assert(key:get_type() == key_type, "incorrect key type"); + return key; + end; + }; +end + +-- RS***, PS*** +local rsa_sign_algos = { RS = "rsassa_pkcs1", PS = "rsassa_pss" }; +local function new_rsa_algorithm(name) + local family, digest_bits = name:match("^(..)(...)$"); + local c_sign = crypto[rsa_sign_algos[family].."_sha"..digest_bits.."_sign"]; + local c_verify = crypto[rsa_sign_algos[family].."_sha"..digest_bits.."_verify"]; + return new_crypto_algorithm(name, "rsaEncryption", c_sign, c_verify); +end + +-- ES*** +local function new_ecdsa_algorithm(name, c_sign, c_verify, sig_bytes) + local function encode_ecdsa_sig(der_sig) + local r, s = crypto.parse_ecdsa_signature(der_sig, sig_bytes); + return r..s; + end + + local expected_sig_length = sig_bytes*2; + local function decode_ecdsa_sig(jwk_sig) + if #jwk_sig ~= expected_sig_length then + return nil; + end + return crypto.build_ecdsa_signature(jwk_sig:sub(1, sig_bytes), jwk_sig:sub(sig_bytes+1)); + end + return new_crypto_algorithm(name, "id-ecPublicKey", c_sign, c_verify, encode_ecdsa_sig, decode_ecdsa_sig); +end + +local algorithms = { + HS256 = new_hmac_algorithm("HS256"), HS384 = new_hmac_algorithm("HS384"), HS512 = new_hmac_algorithm("HS512"); + ES256 = new_ecdsa_algorithm("ES256", crypto.ecdsa_sha256_sign, crypto.ecdsa_sha256_verify, 32); + ES512 = new_ecdsa_algorithm("ES512", crypto.ecdsa_sha512_sign, crypto.ecdsa_sha512_verify, 66); + RS256 = new_rsa_algorithm("RS256"), RS384 = new_rsa_algorithm("RS384"), RS512 = new_rsa_algorithm("RS512"); + PS256 = new_rsa_algorithm("PS256"), PS384 = new_rsa_algorithm("PS384"), PS512 = new_rsa_algorithm("PS512"); }; +local function new_signer(algorithm, key_input, options) + local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm); + local key = (impl.load_private_key or impl.load_key)(key_input); + local sign = impl.sign; + local default_ttl = (options and options.default_ttl) or 3600; + return function (payload) + local issued_at; + if not payload.iat then + issued_at = os.time(); + payload.iat = issued_at; + end + if not payload.exp then + payload.exp = (issued_at or os.time()) + default_ttl; + end + return sign(key, payload); + end +end + +local function new_verifier(algorithm, key_input, options) + local impl = assert(algorithms[algorithm], "Unknown JWT algorithm: "..algorithm); + local key = (impl.load_public_key or impl.load_key)(key_input); + local verify = impl.verify; + local check_expiry = not (options and options.accept_expired); + local claim_verifier = options and options.claim_verifier; + return function (token) + local ok, payload = verify(key, token); + if ok then + local expires_at = check_expiry and payload.exp; + if expires_at then + if type(expires_at) ~= "number" then + return nil, "invalid-expiry"; + elseif expires_at < os.time() then + return nil, "token-expired"; + end + end + if claim_verifier and not claim_verifier(payload) then + return nil, "incorrect-claims"; + end + end + return ok, payload; + end +end + +local function init(algorithm, private_key, public_key, options) + return new_signer(algorithm, private_key, options), new_verifier(algorithm, public_key or private_key, options); +end + +return { + init = init; + new_signer = new_signer; + new_verifier = new_verifier; + -- Exported mainly for tests + _algorithms = algorithms; + -- Deprecated + sign = algorithms.HS256.sign; + verify = algorithms.HS256.verify; +}; + diff -r 54a936345aaa -r 5d9ec2e55d74 util/logger.lua --- a/util/logger.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/logger.lua Wed Mar 27 15:39:03 2024 +0000 @@ -10,6 +10,7 @@ local pairs = pairs; local ipairs = ipairs; local require = require; +local t_remove = table.remove; local _ENV = nil; -- luacheck: std none @@ -71,13 +72,27 @@ end local function add_simple_sink(simple_sink_function, levels) - local format = require "util.format".format; + local format = require "prosody.util.format".format; local function sink_function(name, level, msg, ...) return simple_sink_function(name, level, format(msg, ...)); end for _, level in ipairs(levels or {"debug", "info", "warn", "error"}) do add_level_sink(level, sink_function); end + return sink_function; +end + +local function remove_sink(sink_function) + local removed; + for level, sinks in pairs(level_sinks) do + for i = #sinks, 1, -1 do + if sinks[i] == sink_function then + t_remove(sinks, i); + removed = true; + end + end + end + return removed; end return { @@ -87,4 +102,5 @@ add_level_sink = add_level_sink; add_simple_sink = add_simple_sink; new = make_logger; + remove_sink = remove_sink; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/mathcompat.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/mathcompat.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,13 @@ +if not math.type then + + local function math_type(t) + if type(t) == "number" then + if t % 1 == 0 and t ~= t + 1 and t ~= t - 1 then + return "integer" + else + return "float" + end + end + end + _G.math.type = math_type +end diff -r 54a936345aaa -r 5d9ec2e55d74 util/multitable.lua --- a/util/multitable.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/multitable.lua Wed Mar 27 15:39:03 2024 +0000 @@ -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 +local unpack = table.unpack; local _ENV = nil; -- luacheck: std none diff -r 54a936345aaa -r 5d9ec2e55d74 util/openmetrics.lua --- a/util/openmetrics.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/openmetrics.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,7 @@ --[[ This module implements a subset of the OpenMetrics Internet Draft version 00. -URL: https://tools.ietf.org/html/draft-richih-opsawg-openmetrics-00 +URL: https://datatracker.ietf.org/doc/html/draft-richih-opsawg-openmetrics-00 The following metric types are supported: @@ -19,14 +19,14 @@ -- metric constructor interface: -- metric_ctor(..., family_name, labels, extra) -local time = require "util.time".now; +local time = require "prosody.util.time".now; local select = select; -local array = require "util.array"; -local log = require "util.logger".init("util.openmetrics"); -local new_multitable = require "util.multitable".new; -local iter_multitable = require "util.multitable".iter; +local array = require "prosody.util.array"; +local log = require "prosody.util.logger".init("util.openmetrics"); +local new_multitable = require "prosody.util.multitable".new; +local iter_multitable = require "prosody.util.multitable".iter; local t_concat, t_insert = table.concat, table.insert; -local t_pack, t_unpack = require "util.table".pack, table.unpack or unpack; --luacheck: ignore 113/unpack +local t_pack, t_unpack = table.pack, table.unpack; -- BEGIN of Utility: "metric proxy" -- This allows to wrap a MetricFamily in a proxy which only provides the @@ -35,6 +35,7 @@ -- `with_partial_label` by the moduleapi in order to pre-set the `host` label -- on metrics created in non-global modules. local metric_proxy_mt = {} +metric_proxy_mt.__name = "metric_proxy" metric_proxy_mt.__index = metric_proxy_mt local function new_metric_proxy(metric_family, with_labels_proxy_fun) @@ -128,6 +129,7 @@ -- BEGIN of generic MetricFamily implementation local metric_family_mt = {} +metric_family_mt.__name = "metric_family" metric_family_mt.__index = metric_family_mt local function histogram_metric_ctor(orig_ctor, buckets) @@ -278,6 +280,7 @@ end local metric_registry_mt = {} +metric_registry_mt.__name = "metric_registry" metric_registry_mt.__index = metric_registry_mt local function new_metric_registry(backend) diff -r 54a936345aaa -r 5d9ec2e55d74 util/openssl.lua --- a/util/openssl.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/openssl.lua Wed Mar 27 15:39:03 2024 +0000 @@ -5,7 +5,7 @@ local oid_xmppaddr = "1.3.6.1.5.5.7.8.5"; -- [XMPP-CORE] local oid_dnssrv = "1.3.6.1.5.5.7.8.7"; -- [SRV-ID] -local idna_to_ascii = require "util.encodings".idna.to_ascii; +local idna_to_ascii = require "prosody.util.encodings".idna.to_ascii; local _M = {}; local config = {}; @@ -166,8 +166,7 @@ setmetatable(_M, { __index = function(_, command) return function(opts) - local ret = os_execute(serialize(command, type(opts) == "table" and opts or {})); - return ret == true or ret == 0; + return os_execute(serialize(command, type(opts) == "table" and opts or {})); end; end; }); diff -r 54a936345aaa -r 5d9ec2e55d74 util/paseto.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/paseto.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,218 @@ +local crypto = require "prosody.util.crypto"; +local json = require "prosody.util.json"; +local hashes = require "prosody.util.hashes"; +local base64_encode = require "prosody.util.encodings".base64.encode; +local base64_decode = require "prosody.util.encodings".base64.decode; +local secure_equals = require "prosody.util.hashes".equals; +local bit = require "prosody.util.bitcompat"; +local hex = require "prosody.util.hex"; +local rand = require "prosody.util.random"; +local s_pack = require "prosody.util.struct".pack; + +local s_gsub = string.gsub; + +local v4_public = {}; + +local b64url_rep = { ["+"] = "-", ["/"] = "_", ["="] = "", ["-"] = "+", ["_"] = "/" }; +local function b64url(data) + return (s_gsub(base64_encode(data), "[+/=]", b64url_rep)); +end + +local valid_tails = { + nil; -- Always invalid + "^.[AQgw]$"; -- b??????00 + "^..[AQgwEUk0IYo4Mcs8]$"; -- b????0000 +} + +local function unb64url(data) + local rem = #data%4; + if data:sub(-1,-1) == "=" or rem == 1 or (rem > 1 and not data:sub(-rem):match(valid_tails[rem])) then + return nil; + end + return base64_decode(s_gsub(data, "[-_]", b64url_rep).."=="); +end + +local function le64(n) + return s_pack("= 1 and arg[1] ~= "--help" then - openssl = require "util.openssl"; + openssl = require "prosody.util.openssl"; lfs = require "lfs"; local cert_dir_attrs = lfs.attributes(cert_basedir); if not cert_dir_attrs then @@ -303,7 +303,7 @@ end return cert_commands[subcmd](arg); elseif subcmd == "check" then - return require "util.prosodyctl.check".check({"certs"}); + return require "prosody.util.prosodyctl.check".check({"certs"}); end end pctl.show_usage("cert config|request|generate|key|import", "Helpers for generating X.509 certificates and keys.") diff -r 54a936345aaa -r 5d9ec2e55d74 util/prosodyctl/check.lua --- a/util/prosodyctl/check.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/prosodyctl/check.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,24 +1,24 @@ -local configmanager = require "core.configmanager"; -local moduleapi = require "core.moduleapi"; -local show_usage = require "util.prosodyctl".show_usage; -local show_warning = require "util.prosodyctl".show_warning; -local is_prosody_running = require "util.prosodyctl".isrunning; -local parse_args = require "util.argparse".parse; -local dependencies = require "util.dependencies"; +local configmanager = require "prosody.core.configmanager"; +local moduleapi = require "prosody.core.moduleapi"; +local show_usage = require "prosody.util.prosodyctl".show_usage; +local show_warning = require "prosody.util.prosodyctl".show_warning; +local is_prosody_running = require "prosody.util.prosodyctl".isrunning; +local parse_args = require "prosody.util.argparse".parse; +local dependencies = require "prosody.util.dependencies"; local socket = require "socket"; local socket_url = require "socket.url"; -local jid_split = require "util.jid".prepped_split; -local modulemanager = require "core.modulemanager"; -local async = require "util.async"; -local httputil = require "util.http"; +local jid_split = require "prosody.util.jid".prepped_split; +local modulemanager = require "prosody.core.modulemanager"; +local async = require "prosody.util.async"; +local httputil = require "prosody.util.http"; local function api(host) return setmetatable({ name = "prosodyctl.check"; host = host; log = prosody.log }, { __index = moduleapi }) end local function check_ojn(check_type, target_host) - local http = require "net.http"; -- .new({}); - local json = require "util.json"; + local http = require "prosody.net.http"; -- .new({}); + local json = require "prosody.util.json"; local response, err = async.wait_for(http.request( ("https://observe.jabber.network/api/v1/check/%s"):format(httputil.urlencode(check_type)), @@ -46,7 +46,7 @@ end local function check_probe(base_url, probe_module, target) - local http = require "net.http"; -- .new({}); + local http = require "prosody.net.http"; -- .new({}); local params = httputil.formencode({ module = probe_module; target = target }) local response, err = async.wait_for(http.request(base_url .. "?" .. params)); @@ -67,8 +67,8 @@ end local function check_turn_service(turn_service, ping_service) - local ip = require "util.ip"; - local stun = require "net.stun"; + local ip = require "prosody.util.ip"; + local stun = require "prosody.net.stun"; -- Create UDP socket for communication with the server local sock = assert(require "socket".udp()); @@ -160,7 +160,7 @@ result.error = "TURN server did not response to allocation request: "..err; return result; elseif alloc_response:is_err_resp() then - result.error = ("TURN allocation failed: %d (%s)"):format(alloc_response:get_error()); + result.error = ("TURN server failed to create allocation: %d (%s)"):format(alloc_response:get_error()); return result; elseif not alloc_response:is_success_resp() then result.error = ("Unexpected TURN response: %d (%s)"):format(alloc_response:get_type()); @@ -309,18 +309,15 @@ print("Error: Unknown parameter: "..opts_info); return 1; end - local array = require "util.array"; - local set = require "util.set"; - local it = require "util.iterators"; + local array = require "prosody.util.array"; + local set = require "prosody.util.set"; + local it = require "prosody.util.iterators"; local ok = true; + local function contains_match(hayset, needle) for member in hayset do if member:find(needle) then return true end end end local function disabled_hosts(host, conf) return host ~= "*" and conf.enabled ~= false; end local function enabled_hosts() return it.filter(disabled_hosts, pairs(configmanager.getconfig())); end - if not (what == nil or what == "disabled" or what == "config" or what == "dns" or what == "certs" or what == "connectivity" or what == "turn") then - show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs', 'disabled', 'turn' or 'connectivity'.", what); - show_warning("Note: The connectivity check will connect to a remote server."); - return 1; - end - if not what or what == "disabled" then + local checks = {}; + function checks.disabled() local disabled_hosts_set = set.new(); for host in it.filter("*", pairs(configmanager.getconfig())) do if api(host):get_option_boolean("enabled") == false then @@ -335,7 +332,7 @@ print"" end end - if not what or what == "config" then + function checks.config() print("Checking config..."); if what == "config" then @@ -510,9 +507,9 @@ end for k, v in pairs(modules) do if type(k) ~= "number" or type(v) ~= "string" then - print(" The " .. name .. " in the " .. host .. " section should not be a map of " .. type(k) .. " to " .. type(v) - .. " but a list of strings, e.g."); + print(" The " .. name .. " in the " .. host .. " section should be a list of strings, e.g."); print(" " .. name .. " = { \"name_of_module\", \"another_plugin\", }") + print(" It should not contain key = value pairs, try putting them outside the {} brackets."); ok = false break end @@ -740,7 +737,7 @@ -- Check hostname validity do - local idna = require "util.encodings".idna; + local idna = require "prosody.util.encodings".idna; local invalid_hosts = {}; local alabel_hosts = {}; for host in it.filter("*", pairs(configmanager.getconfig())) do @@ -791,14 +788,14 @@ print("Done.\n"); end - if not what or what == "dns" then - local dns = require "net.dns"; + function checks.dns() + local dns = require "prosody.net.dns"; pcall(function () - local unbound = require"net.unbound"; + local unbound = require"prosody.net.unbound"; dns = unbound.dns; end) - local idna = require "util.encodings".idna; - local ip = require "util.ip"; + local idna = require "prosody.util.encodings".idna; + local ip = require "prosody.util.ip"; local global = api("*"); local c2s_ports = global:get_option_set("c2s_ports", {5222}); local s2s_ports = global:get_option_set("s2s_ports", {5269}); @@ -861,7 +858,7 @@ end end - local local_addresses = require"util.net".local_addresses() or {}; + local local_addresses = require"prosody.util.net".local_addresses() or {}; for addr in it.values(local_addresses) do if not ip.new_ip(addr).private then @@ -1028,9 +1025,6 @@ end local known_http_modules = set.new { "bosh"; "http_files"; "http_file_share"; "http_openmetrics"; "websocket" }; - local function contains_match(hayset, needle) - for member in hayset do if member:find(needle) then return true end end - end if modules:contains("http") or not set.intersection(modules, known_http_modules):empty() or contains_match(modules, "^http_") or contains_match(modules, "_web$") then @@ -1166,11 +1160,14 @@ ok = false; end end - if not what or what == "certs" then + function checks.certs() local cert_ok; print"Checking certificates..." - local x509_verify_identity = require"util.x509".verify_identity; - local create_context = require "core.certmanager".create_context; + local x509_verify_identity = require"prosody.util.x509".verify_identity; + local use_dane = configmanager.get("*", "use_dane"); + local pem2der = require"prosody.util.x509".pem2der; + local sha256 = require"prosody.util.hashes".sha256; + local create_context = require "prosody.core.certmanager".create_context; local ssl = dependencies.softreq"ssl"; -- local datetime_parse = require"util.datetime".parse_x509; local load_cert = ssl and ssl.loadcertificate; @@ -1183,13 +1180,14 @@ cert_ok = false else for host in it.filter(skip_bare_jid_hosts, enabled_hosts()) do + local modules = modulemanager.get_modules_for_host(host); print("Checking certificate for "..host); -- First, let's find out what certificate this host uses. local host_ssl_config = configmanager.rawget(host, "ssl") or configmanager.rawget(host:match("%.(.*)"), "ssl"); local global_ssl_config = configmanager.rawget("*", "ssl"); - local ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config); - if not ok then + local ctx_ok, err, ssl_config = create_context(host, "server", host_ssl_config, global_ssl_config); + if not ctx_ok then print(" Error: "..err); cert_ok = false elseif not ssl_config.certificate then @@ -1224,17 +1222,39 @@ elseif not cert:validat(os.time() + 86400*31) then print(" Certificate expires within one month.") end - if select(2, modulemanager.get_modules_for_host(host)) == nil - and not x509_verify_identity(host, "_xmpp-client", cert) then + if modules:contains("c2s") and not x509_verify_identity(host, "_xmpp-client", cert) then print(" Not valid for client connections to "..host..".") cert_ok = false end - if (not (api(host):get_option_boolean("anonymous_login", false) - or api(host):get_option_string("authentication", "internal_hashed") == "anonymous")) - and not x509_verify_identity(host, "_xmpp-server", cert) then + local anon = api(host):get_option_string("authentication", "internal_hashed") == "anonymous"; + local anon_s2s = api(host):get_option_boolean("allow_anonymous_s2s", false); + if modules:contains("s2s") and (anon_s2s or not anon) and not x509_verify_identity(host, "_xmpp-server", cert) then print(" Not valid for server-to-server connections to "..host..".") cert_ok = false end + + local known_http_modules = set.new { "bosh"; "http_files"; "http_file_share"; "http_openmetrics"; "websocket" }; + local http_loaded = modules:contains("http") + or not set.intersection(modules, known_http_modules):empty() + or contains_match(modules, "^http_") + or contains_match(modules, "_web$"); + + local http_host = api(host):get_option_string("http_host", host); + if api(host):get_option_string("http_external_url") then + -- Assumed behind a reverse proxy + http_loaded = false; + end + if http_loaded and not x509_verify_identity(http_host, nil, cert) then + print(" Not valid for HTTPS connections to "..host..".") + cert_ok = false + end + if use_dane then + if cert.pubkey then + print(" DANE: TLSA 3 1 1 "..sha256(pem2der(cert:pubkey()), true)) + elseif cert.pem then + print(" DANE: TLSA 3 0 1 "..sha256(pem2der(cert:pem()), true)) + end + end end end end @@ -1247,7 +1267,7 @@ print("") end -- intentionally not doing this by default - if what == "connectivity" then + function checks.connectivity() local _, prosody_is_running = is_prosody_running(); if api("*"):get_option_string("pidfile") and not prosody_is_running then print("Prosody does not appear to be running, which is required for this test."); @@ -1339,7 +1359,7 @@ print("Note: It does not ensure that the check actually reaches this specific prosody instance.") end - if not what or what == "turn" then + function checks.turn() local turn_enabled_hosts = {}; local turn_services = {}; @@ -1414,6 +1434,26 @@ end end end + if what == nil or what == "all" then + local ret; + ret = checks.disabled(); + if ret ~= nil then return ret; end + ret = checks.config(); + if ret ~= nil then return ret; end + ret = checks.dns(); + if ret ~= nil then return ret; end + ret = checks.certs(); + if ret ~= nil then return ret; end + ret = checks.turn(); + if ret ~= nil then return ret; end + elseif checks[what] then + local ret = checks[what](); + if ret ~= nil then return ret; end + else + show_warning("Don't know how to check '%s'. Try one of 'config', 'dns', 'certs', 'disabled', 'turn' or 'connectivity'.", what); + show_warning("Note: The connectivity check will connect to a remote server."); + return 1; + end if not ok then print("Problems found, see above."); diff -r 54a936345aaa -r 5d9ec2e55d74 util/prosodyctl/shell.lua --- a/util/prosodyctl/shell.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/prosodyctl/shell.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,13 +1,15 @@ -local config = require "core.configmanager"; -local server = require "net.server"; -local st = require "util.stanza"; -local path = require "util.paths"; -local parse_args = require "util.argparse".parse; -local unpack = table.unpack or _G.unpack; +local config = require "prosody.core.configmanager"; +local server = require "prosody.net.server"; +local st = require "prosody.util.stanza"; +local path = require "prosody.util.paths"; +local parse_args = require "prosody.util.argparse".parse; +local tc = require "prosody.util.termcolours"; +local isatty = require "prosody.util.pposix".isatty; +local term_width = require"prosody.util.human.io".term_width; local have_readline, readline = pcall(require, "readline"); -local adminstream = require "util.adminstream"; +local adminstream = require "prosody.util.adminstream"; if have_readline then readline.set_readline_name("prosody"); @@ -27,7 +29,7 @@ end local function send_line(client, line) - client.send(st.stanza("repl-input"):text(line)); + client.send(st.stanza("repl-input", { width = tostring(term_width()) }):text(line)); end local function repl(client) @@ -64,6 +66,7 @@ local function start(arg) --luacheck: ignore 212/arg local client = adminstream.client(); local opts, err, where = parse_args(arg); + local ttyout = isatty(io.stdout); if not opts then if err == "param-not-found" then @@ -76,24 +79,36 @@ if arg[1] then if arg[2] then - -- prosodyctl shell module reload foo bar.com --> module:reload("foo", "bar.com") - -- COMPAT Lua 5.1 doesn't have the separator argument to string.rep - arg[1] = string.format("%s:%s("..string.rep("%q, ", #arg-2):sub(1, -3)..")", unpack(arg)); + local fmt = { "%s"; ":%s("; ")" }; + for i = 3, #arg do + if arg[i]:sub(1, 1) == ":" then + table.insert(fmt, i, ")%s("); + elseif i > 3 and fmt[i - 1] == "%q" then + table.insert(fmt, i, ", %q"); + else + table.insert(fmt, i, "%q"); + end + end + arg[1] = string.format(table.concat(fmt), table.unpack(arg)); end client.events.add_handler("connected", function() - client.send(st.stanza("repl-input"):text(arg[1])); + send_line(client, arg[1]); return true; end, 1); local errors = 0; -- TODO This is weird, but works for now. client.events.add_handler("received", function(stanza) if stanza.name == "repl-output" or stanza.name == "repl-result" then + local dest = io.stdout; if stanza.attr.type == "error" then errors = errors + 1; - io.stderr:write(stanza:get_text(), "\n"); + dest = io.stderr; + end + if stanza.attr.eol == "0" then + dest:write(stanza:get_text()); else - print(stanza:get_text()); + dest:write(stanza:get_text(), "\n"); end end if stanza.name == "repl-result" then @@ -118,7 +133,11 @@ client.events.add_handler("received", function (stanza) if stanza.name == "repl-output" or stanza.name == "repl-result" then local result_prefix = stanza.attr.type == "error" and "!" or "|"; - print(result_prefix.." "..stanza:get_text()); + local out = result_prefix.." "..stanza:get_text(); + if ttyout and stanza.attr.type == "error" then + out = tc.getstring(tc.getstyle("red"), out); + end + print(out); end if stanza.name == "repl-result" then repl(client); diff -r 54a936345aaa -r 5d9ec2e55d74 util/pubsub.lua --- a/util/pubsub.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/pubsub.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,6 @@ -local events = require "util.events"; -local cache = require "util.cache"; -local errors = require "util.error"; +local events = require "prosody.util.events"; +local cache = require "prosody.util.cache"; +local errors = require "prosody.util.error"; local service_mt = {}; diff -r 54a936345aaa -r 5d9ec2e55d74 util/queue.lua --- a/util/queue.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/queue.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,7 +9,7 @@ -- Small ringbuffer library (i.e. an efficient FIFO queue with a size limit) -- (because unbounded dynamically-growing queues are a bad thing...) -local have_utable, utable = pcall(require, "util.table"); -- For pre-allocation of table +local have_utable, utable = pcall(require, "prosody.util.table"); -- For pre-allocation of table local function new(size, allow_wrapping) -- Head is next insert, tail is next read diff -r 54a936345aaa -r 5d9ec2e55d74 util/random.lua --- a/util/random.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/random.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,7 +6,7 @@ -- COPYING file in the source package for more information. -- -local ok, crand = pcall(require, "util.crand"); +local ok, crand = pcall(require, "prosody.util.crand"); if ok and pcall(crand.bytes, 1) then return crand; end local urandom, urandom_err = io.open("/dev/urandom", "r"); diff -r 54a936345aaa -r 5d9ec2e55d74 util/rfc6724.lua --- a/util/rfc6724.lua Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,141 +0,0 @@ --- Prosody IM --- Copyright (C) 2011-2013 Florian Zeitz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - --- This is used to sort destination addresses by preference --- during S2S connections. --- We can't hand this off to getaddrinfo, since it blocks - -local ip_commonPrefixLength = require"util.ip".commonPrefixLength - -local function commonPrefixLength(ipA, ipB) - local len = ip_commonPrefixLength(ipA, ipB); - return len < 64 and len or 64; -end - -local function t_sort(t, comp) - for i = 1, (#t - 1) do - for j = (i + 1), #t do - local a, b = t[i], t[j]; - if not comp(a,b) then - t[i], t[j] = b, a; - end - end - end -end - -local function source(dest, candidates) - local function comp(ipA, ipB) - -- Rule 1: Prefer same address - if dest == ipA then - return true; - elseif dest == ipB then - return false; - end - - -- Rule 2: Prefer appropriate scope - if ipA.scope < ipB.scope then - if ipA.scope < dest.scope then - return false; - else - return true; - end - elseif ipA.scope > ipB.scope then - if ipB.scope < dest.scope then - return true; - else - return false; - end - end - - -- Rule 3: Avoid deprecated addresses - -- XXX: No way to determine this - -- Rule 4: Prefer home addresses - -- XXX: Mobility Address related, no way to determine this - -- Rule 5: Prefer outgoing interface - -- XXX: Interface to address relation. No way to determine this - -- Rule 6: Prefer matching label - if ipA.label == dest.label and ipB.label ~= dest.label then - return true; - elseif ipB.label == dest.label and ipA.label ~= dest.label then - return false; - end - - -- Rule 7: Prefer temporary addresses (over public ones) - -- XXX: No way to determine this - -- Rule 8: Use longest matching prefix - if commonPrefixLength(ipA, dest) > commonPrefixLength(ipB, dest) then - return true; - else - return false; - end - end - - t_sort(candidates, comp); - return candidates[1]; -end - -local function destination(candidates, sources) - local sourceAddrs = {}; - local function comp(ipA, ipB) - local ipAsource = sourceAddrs[ipA]; - local ipBsource = sourceAddrs[ipB]; - -- Rule 1: Avoid unusable destinations - -- XXX: No such information - -- Rule 2: Prefer matching scope - if ipA.scope == ipAsource.scope and ipB.scope ~= ipBsource.scope then - return true; - elseif ipA.scope ~= ipAsource.scope and ipB.scope == ipBsource.scope then - return false; - end - - -- Rule 3: Avoid deprecated addresses - -- XXX: No way to determine this - -- Rule 4: Prefer home addresses - -- XXX: Mobility Address related, no way to determine this - -- Rule 5: Prefer matching label - if ipAsource.label == ipA.label and ipBsource.label ~= ipB.label then - return true; - elseif ipBsource.label == ipB.label and ipAsource.label ~= ipA.label then - return false; - end - - -- Rule 6: Prefer higher precedence - if ipA.precedence > ipB.precedence then - return true; - elseif ipA.precedence < ipB.precedence then - return false; - end - - -- Rule 7: Prefer native transport - -- XXX: No way to determine this - -- Rule 8: Prefer smaller scope - if ipA.scope < ipB.scope then - return true; - elseif ipA.scope > ipB.scope then - return false; - end - - -- Rule 9: Use longest matching prefix - if commonPrefixLength(ipA, ipAsource) > commonPrefixLength(ipB, ipBsource) then - return true; - elseif commonPrefixLength(ipA, ipAsource) < commonPrefixLength(ipB, ipBsource) then - return false; - end - - -- Rule 10: Otherwise, leave order unchanged - return true; - end - for _, ip in ipairs(candidates) do - sourceAddrs[ip] = source(ip, sources); - end - - t_sort(candidates, comp); - return candidates; -end - -return {source = source, - destination = destination}; diff -r 54a936345aaa -r 5d9ec2e55d74 util/roles.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/roles.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,123 @@ +local array = require "prosody.util.array"; +local it = require "prosody.util.iterators"; +local new_short_id = require "prosody.util.id".short; + +local role_methods = {}; +local role_mt = { + __index = role_methods; + __name = "role"; + __add = nil; +}; + +local function is_role(o) + local mt = getmetatable(o); + return mt == role_mt; +end + +local function _new_may(permissions, inherited_mays) + local n_inherited = inherited_mays and #inherited_mays; + return function (role, action, context) + -- Note: 'role' may be a descendent role, not only the one we're attached to + local policy = permissions[action]; + if policy ~= nil then + return policy; + end + if n_inherited then + for i = 1, n_inherited do + policy = inherited_mays[i](role, action, context); + if policy ~= nil then + return policy; + end + end + end + return nil; + end +end + +local permissions_key = {}; + +-- { +-- Required: +-- name = "My fancy role"; +-- +-- Optional: +-- inherits = { role_obj... } +-- default = true +-- priority = 100 +-- permissions = { +-- ["foo"] = true; -- allow +-- ["bar"] = false; -- deny +-- } +-- } +local function new(base_config, overrides) + local config = setmetatable(overrides or {}, { __index = base_config }); + local permissions = {}; + local inherited_mays; + if config.inherits then + inherited_mays = array.pluck(config.inherits, "may"); + end + local new_role = { + id = new_short_id(); + name = config.name; + description = config.description; + default = config.default; + priority = config.priority; + may = _new_may(permissions, inherited_mays); + inherits = config.inherits; + [permissions_key] = permissions; + }; + local desired_permissions = config.permissions or config[permissions_key]; + for k, v in pairs(desired_permissions or {}) do + permissions[k] = v; + end + return setmetatable(new_role, role_mt); +end + +function role_mt:__freeze() + local t = { + id = self.id; + name = self.name; + description = self.description; + default = self.default; + priority = self.priority; + inherits = self.inherits; + permissions = self[permissions_key]; + }; + return t; +end + +function role_methods:clone(overrides) + return new(self, overrides); +end + +function role_methods:set_permission(permission_name, policy, overwrite) + local permissions = self[permissions_key]; + if overwrite ~= true and permissions[permission_name] ~= nil and permissions[permission_name] ~= policy then + return false, "policy-already-exists"; + end + permissions[permission_name] = policy; + return true; +end + +function role_methods:policies() + local policy_iterator, s, v = it.join(pairs(self[permissions_key])); + if self.inherits then + for _, inherited_role in ipairs(self.inherits) do + policy_iterator:append(inherited_role:policies()); + end + end + return policy_iterator, s, v; +end + +function role_mt.__tostring(self) + return ("role<[%s] %s>"):format(self.id or "nil", self.name or "[no name]"); +end + +function role_mt.__pairs(self) + return it.filter(permissions_key, next, self); +end + +return { + is_role = is_role; + new = new; +}; diff -r 54a936345aaa -r 5d9ec2e55d74 util/rsm.lua --- a/util/rsm.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/rsm.lua Wed Mar 27 15:39:03 2024 +0000 @@ -9,7 +9,7 @@ -- XEP-0313: Message Archive Management for Prosody -- -local stanza = require"util.stanza".stanza; +local stanza = require"prosody.util.stanza".stanza; local tonumber = tonumber; local s_format = string.format; local type = type; diff -r 54a936345aaa -r 5d9ec2e55d74 util/sasl.lua --- a/util/sasl.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/sasl.lua Wed Mar 27 15:39:03 2024 +0000 @@ -47,7 +47,7 @@ local backend_mechanism = {}; local mechanism_channelbindings = {}; --- register a new SASL mechanisms +-- register a new SASL mechanism local function registerMechanism(name, backends, f, cb_backends) assert(type(name) == "string", "Parameter name MUST be a string."); assert(type(backends) == "string" or type(backends) == "table", "Parameter backends MUST be either a string or a table."); @@ -133,10 +133,11 @@ end -- load the mechanisms -require "util.sasl.plain" .init(registerMechanism); -require "util.sasl.anonymous" .init(registerMechanism); -require "util.sasl.scram" .init(registerMechanism); -require "util.sasl.external" .init(registerMechanism); +require "prosody.util.sasl.plain" .init(registerMechanism); +require "prosody.util.sasl.anonymous" .init(registerMechanism); +require "prosody.util.sasl.oauthbearer" .init(registerMechanism); +require "prosody.util.sasl.scram" .init(registerMechanism); +require "prosody.util.sasl.external" .init(registerMechanism); return { registerMechanism = registerMechanism; diff -r 54a936345aaa -r 5d9ec2e55d74 util/sasl/anonymous.lua --- a/util/sasl/anonymous.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/sasl/anonymous.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,7 +12,7 @@ -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -local generate_random_id = require "util.id".medium; +local generate_random_id = require "prosody.util.id".medium; local _ENV = nil; -- luacheck: std none @@ -33,8 +33,8 @@ local username; repeat username = generate_random_id():lower(); - until self.profile.anonymous(self, username, self.realm); - self.username = username; + self.username = username; + until self.profile.anonymous(self, username, self.realm, message); return "success" end diff -r 54a936345aaa -r 5d9ec2e55d74 util/sasl/external.lua --- a/util/sasl/external.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/sasl/external.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,4 @@ -local saslprep = require "util.encodings".stringprep.saslprep; +local saslprep = require "prosody.util.encodings".stringprep.saslprep; local _ENV = nil; -- luacheck: std none diff -r 54a936345aaa -r 5d9ec2e55d74 util/sasl/oauthbearer.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/sasl/oauthbearer.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,62 @@ +local json = require "prosody.util.json"; +local _ENV = nil; + + +local function oauthbearer(self, message) + if not message then + return "failure", "malformed-request"; + end + + if message == "\001" then + return "failure", "not-authorized"; + end + + -- gs2-header kvsep *kvpair kvsep + local gs2_header, kvpairs = message:match("^(n,[^,]*,)\001(.+)\001$"); + if not gs2_header then + return "failure", "malformed-request"; + end + local gs2_authzid = gs2_header:match("^[^,]*,a=([^,]*),$"); + + -- key "=" value kvsep + local auth_header; + for k, v in kvpairs:gmatch("([a-zA-Z]+)=([\033-\126 \009\r\n]*)\001") do + if k == "auth" then + auth_header = v; + break; + end + end + + if not auth_header then + return "failure", "malformed-request"; + end + + local token = auth_header:match("^Bearer (.+)$"); + + local username, state, token_info = self.profile.oauthbearer(self, token, self.realm, gs2_authzid); + + if state == false then + return "failure", "account-disabled"; + elseif state == nil or not username then + -- For token-level errors, RFC 7628 demands use of a JSON-encoded + -- challenge response upon failure. We relay additional info from + -- the auth backend if available. + return "challenge", json.encode({ + status = token_info and token_info.status or "invalid_token"; + scope = token_info and token_info.scope or nil; + ["openid-configuration"] = token_info and token_info.oidc_discovery_url or nil; + }); + end + self.username = username; + self.token_info = token_info; + + return "success"; +end + +local function init(registerMechanism) + registerMechanism("OAUTHBEARER", {"oauthbearer"}, oauthbearer); +end + +return { + init = init; +} diff -r 54a936345aaa -r 5d9ec2e55d74 util/sasl/plain.lua --- a/util/sasl/plain.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/sasl/plain.lua Wed Mar 27 15:39:03 2024 +0000 @@ -12,9 +12,9 @@ -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. local s_match = string.match; -local saslprep = require "util.encodings".stringprep.saslprep; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local log = require "util.logger".init("sasl"); +local saslprep = require "prosody.util.encodings".stringprep.saslprep; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local log = require "prosody.util.logger".init("sasl"); local _ENV = nil; -- luacheck: std none @@ -69,10 +69,10 @@ local correct, state = false, false; if self.profile.plain then local correct_password; - correct_password, state = self.profile.plain(self, authentication, self.realm); + correct_password, state = self.profile.plain(self, authentication, self.realm, authorization); correct = (saslprep(correct_password) == password); elseif self.profile.plain_test then - correct, state = self.profile.plain_test(self, authentication, password, self.realm); + correct, state = self.profile.plain_test(self, authentication, password, self.realm, authorization); end if state == false then diff -r 54a936345aaa -r 5d9ec2e55d74 util/sasl/scram.lua --- a/util/sasl/scram.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/sasl/scram.lua Wed Mar 27 15:39:03 2024 +0000 @@ -13,13 +13,13 @@ local s_match = string.match; local type = type -local base64 = require "util.encodings".base64; -local hashes = require "util.hashes"; -local generate_uuid = require "util.uuid".generate; -local saslprep = require "util.encodings".stringprep.saslprep; -local nodeprep = require "util.encodings".stringprep.nodeprep; -local log = require "util.logger".init("sasl"); -local binaryXOR = require "util.strbitop".sxor; +local base64 = require "prosody.util.encodings".base64; +local hashes = require "prosody.util.hashes"; +local generate_uuid = require "prosody.util.uuid".generate; +local saslprep = require "prosody.util.encodings".stringprep.saslprep; +local nodeprep = require "prosody.util.encodings".stringprep.nodeprep; +local log = require "prosody.util.logger".init("sasl"); +local binaryXOR = require "prosody.util.strbitop".sxor; local _ENV = nil; -- luacheck: std none @@ -101,7 +101,6 @@ local client_first_message = message; -- TODO: fail if authzid is provided, since we don't support them yet - -- luacheck: ignore 211/authzid local gs2_header, gs2_cbind_flag, gs2_cbind_name, authzid, client_first_message_bare, username, clientnonce = s_match(client_first_message, "^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$"); @@ -112,8 +111,8 @@ if support_channel_binding and gs2_cbind_flag == "y" then -- "y" -> client does support channel binding -- but thinks the server does not. - return "failure", "malformed-request"; - end + return "failure", "malformed-request"; + end if gs2_cbind_flag == "n" then -- "n" -> client doesn't support channel binding. @@ -144,7 +143,7 @@ -- retrieve credentials local stored_key, server_key, salt, iteration_count; if self.profile.plain then - local password, status = self.profile.plain(self, username, self.realm) + local password, status = self.profile.plain(self, username, self.realm, authzid) if status == nil then return "failure", "not-authorized" elseif status == false then return "failure", "account-disabled" end @@ -165,7 +164,7 @@ end elseif self.profile[profile_name] then local status; - stored_key, server_key, iteration_count, salt, status = self.profile[profile_name](self, username, self.realm); + stored_key, server_key, iteration_count, salt, status = self.profile[profile_name](self, username, self.realm, authzid); if status == nil then return "failure", "not-authorized" elseif status == false then return "failure", "account-disabled" end end @@ -240,7 +239,7 @@ -- register channel binding equivalent registerMechanism("SCRAM-"..hash_name.."-PLUS", {"plain", "scram_"..(hashprep(hash_name))}, - scram_gen(hash_name:lower(), hash, hmac_hash, get_auth_db, true), {"tls-unique"}); + scram_gen(hash_name:lower(), hash, hmac_hash, get_auth_db, true), {"tls-unique", "tls-exporter"}); end registerSCRAMMechanism("SHA-1", hashes.sha1, hashes.hmac_sha1, hashes.pbkdf2_hmac_sha1); diff -r 54a936345aaa -r 5d9ec2e55d74 util/serialization.lua --- a/util/serialization.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/serialization.lua Wed Mar 27 15:39:03 2024 +0000 @@ -16,15 +16,17 @@ local s_match = string.match; local t_concat = table.concat; -local to_hex = require "util.hex".to; +local to_hex = require "prosody.util.hex".to; local pcall = pcall; -local envload = require"util.envload".envload; +local envload = require"prosody.util.envload".envload; + +if not math.type then + require "prosody.util.mathcompat" +end local pos_inf, neg_inf = math.huge, -math.huge; -local m_type = math.type or function (n) - return n % 1 == 0 and n <= 9007199254740992 and n >= -9007199254740992 and "integer" or "float"; -end; +local m_type = math.type; local function rawpairs(t) return next, t, nil; @@ -94,6 +96,10 @@ opt.itemlast = opt.itemlast or ""; opt.equals = opt.equals or "="; opt.unquoted = true; + elseif opt.preset == "pretty" then + opt.fatal = false; + opt.freeze = true; + opt.unquoted = true; end local fallback = opt.fallback or opt.fatal == false and nonfatal_fallback or fatal_error; diff -r 54a936345aaa -r 5d9ec2e55d74 util/session.lua --- a/util/session.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/session.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,12 @@ -local initialize_filters = require "util.filters".initialize; -local logger = require "util.logger"; +local initialize_filters = require "prosody.util.filters".initialize; +local time = require "prosody.util.time"; +local logger = require "prosody.util.logger"; local function new_session(typ) local session = { type = typ .. "_unauthed"; base_type = typ; + since = time.now(); }; return session; end @@ -57,10 +59,16 @@ return session; end +local function set_role(session, role) + session.role = role; +end + return { new = new_session; + set_id = set_id; set_logger = set_logger; set_conn = set_conn; set_send = set_send; + set_role = set_role; } diff -r 54a936345aaa -r 5d9ec2e55d74 util/set.lua --- a/util/set.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/set.lua Wed Mar 27 15:39:03 2024 +0000 @@ -186,7 +186,7 @@ for item in pairs(items) do s[#s+1] = tostring(item); end - return t_concat(s, ", "); + return "{"..t_concat(s, ", ").."}"; end return { diff -r 54a936345aaa -r 5d9ec2e55d74 util/smqueue.lua --- a/util/smqueue.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/smqueue.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,4 +1,4 @@ -local queue = require("util.queue"); +local queue = require("prosody.util.queue"); local lib = { smqueue = {} } diff -r 54a936345aaa -r 5d9ec2e55d74 util/sql.lua --- a/util/sql.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/sql.lua Wed Mar 27 15:39:03 2024 +0000 @@ -4,9 +4,9 @@ local tostring = tostring; local type = type; local assert, pcall, debug_traceback = assert, pcall, debug.traceback; -local xpcall = require "util.xpcall".xpcall; +local xpcall = require "prosody.util.xpcall".xpcall; local t_concat = table.concat; -local log = require "util.logger".init("sql"); +local log = require "prosody.util.logger".init("sql"); local DBI = require "DBI"; -- This loads all available drivers while globals are unlocked @@ -27,8 +27,6 @@ local function is_index(x) return getmetatable(x)==index_mt; end local function is_table(x) return getmetatable(x)==table_mt; end local function is_query(x) return getmetatable(x)==query_mt; end -local function Integer() return "Integer()" end -local function String() return "String()" end local function Column(definition) return setmetatable(definition, column_mt); @@ -99,6 +97,9 @@ function engine:onconnect() -- luacheck: ignore 212/self -- Override from create_engine() end +function engine:ondisconnect() -- luacheck: ignore 212/self + -- Override from create_engine() +end function engine:prepquery(sql) if self.params.driver == "MySQL" then @@ -224,6 +225,7 @@ if not conn or not conn:ping() then log("debug", "Database connection was closed. Will reconnect and retry."); self.conn = nil; + self:ondisconnect(); log("debug", "Retrying SQL transaction [%s]", (...)); ok, ret, b, c = self:_transaction(...); log("debug", "SQL transaction retry %s", ok and "succeeded" or "failed"); @@ -365,8 +367,8 @@ }; end -local function create_engine(_, params, onconnect) - return setmetatable({ url = db2uri(params), params = params, onconnect = onconnect }, engine_mt); +local function create_engine(_, params, onconnect, ondisconnect) + return setmetatable({ url = db2uri(params); params = params; onconnect = onconnect; ondisconnect = ondisconnect }, engine_mt); end return { @@ -374,8 +376,6 @@ is_index = is_index; is_table = is_table; is_query = is_query; - Integer = Integer; - String = String; Column = Column; Table = Table; Index = Index; diff -r 54a936345aaa -r 5d9ec2e55d74 util/sqlite3.lua --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/util/sqlite3.lua Wed Mar 27 15:39:03 2024 +0000 @@ -0,0 +1,370 @@ + +local setmetatable, getmetatable = setmetatable, getmetatable; +local ipairs, select = ipairs, select; +local tostring = tostring; +local assert, xpcall, debug_traceback = assert, xpcall, debug.traceback; +local error = error +local type = type +local t_concat = table.concat; +local array = require "prosody.util.array"; +local log = require "prosody.util.logger".init("sql"); + +local lsqlite3 = require "lsqlite3"; +local build_url = require "socket.url".build; + +-- from sqlite3.h, no copyright claimed +local sqlite_errors = require"prosody.util.error".init("util.sqlite3", { + -- FIXME xmpp error conditions? + [1] = { code = 1; type = "modify"; condition = "ERROR"; text = "Generic error" }; + [2] = { code = 2; type = "cancel"; condition = "INTERNAL"; text = "Internal logic error in SQLite" }; + [3] = { code = 3; type = "auth"; condition = "PERM"; text = "Access permission denied" }; + [4] = { code = 4; type = "cancel"; condition = "ABORT"; text = "Callback routine requested an abort" }; + [5] = { code = 5; type = "wait"; condition = "BUSY"; text = "The database file is locked" }; + [6] = { code = 6; type = "wait"; condition = "LOCKED"; text = "A table in the database is locked" }; + [7] = { code = 7; type = "wait"; condition = "NOMEM"; text = "A malloc() failed" }; + [8] = { code = 8; type = "cancel"; condition = "READONLY"; text = "Attempt to write a readonly database" }; + [9] = { code = 9; type = "cancel"; condition = "INTERRUPT"; text = "Operation terminated by sqlite3_interrupt()" }; + [10] = { code = 10; type = "wait"; condition = "IOERR"; text = "Some kind of disk I/O error occurred" }; + [11] = { code = 11; type = "cancel"; condition = "CORRUPT"; text = "The database disk image is malformed" }; + [12] = { code = 12; type = "modify"; condition = "NOTFOUND"; text = "Unknown opcode in sqlite3_file_control()" }; + [13] = { code = 13; type = "wait"; condition = "FULL"; text = "Insertion failed because database is full" }; + [14] = { code = 14; type = "auth"; condition = "CANTOPEN"; text = "Unable to open the database file" }; + [15] = { code = 15; type = "cancel"; condition = "PROTOCOL"; text = "Database lock protocol error" }; + [16] = { code = 16; type = "continue"; condition = "EMPTY"; text = "Internal use only" }; + [17] = { code = 17; type = "modify"; condition = "SCHEMA"; text = "The database schema changed" }; + [18] = { code = 18; type = "modify"; condition = "TOOBIG"; text = "String or BLOB exceeds size limit" }; + [19] = { code = 19; type = "modify"; condition = "CONSTRAINT"; text = "Abort due to constraint violation" }; + [20] = { code = 20; type = "modify"; condition = "MISMATCH"; text = "Data type mismatch" }; + [21] = { code = 21; type = "modify"; condition = "MISUSE"; text = "Library used incorrectly" }; + [22] = { code = 22; type = "cancel"; condition = "NOLFS"; text = "Uses OS features not supported on host" }; + [23] = { code = 23; type = "auth"; condition = "AUTH"; text = "Authorization denied" }; + [24] = { code = 24; type = "modify"; condition = "FORMAT"; text = "Not used" }; + [25] = { code = 25; type = "modify"; condition = "RANGE"; text = "2nd parameter to sqlite3_bind out of range" }; + [26] = { code = 26; type = "cancel"; condition = "NOTADB"; text = "File opened that is not a database file" }; + [27] = { code = 27; type = "continue"; condition = "NOTICE"; text = "Notifications from sqlite3_log()" }; + [28] = { code = 28; type = "continue"; condition = "WARNING"; text = "Warnings from sqlite3_log()" }; + [100] = { code = 100; type = "continue"; condition = "ROW"; text = "sqlite3_step() has another row ready" }; + [101] = { code = 101; type = "continue"; condition = "DONE"; text = "sqlite3_step() has finished executing" }; +}); + +-- luacheck: ignore 411/assert +local assert = function(cond, errno, err) + return assert(sqlite_errors.coerce(cond, err or errno)); +end +local _ENV = nil; +-- luacheck: std none + +local column_mt = {}; +local table_mt = {}; +local query_mt = {}; +--local op_mt = {}; +local index_mt = {}; + +local function is_column(x) return getmetatable(x)==column_mt; end +local function is_index(x) return getmetatable(x)==index_mt; end +local function is_table(x) return getmetatable(x)==table_mt; end +local function is_query(x) return getmetatable(x)==query_mt; end + +local function Column(definition) + return setmetatable(definition, column_mt); +end +local function Table(definition) + local c = {} + for i,col in ipairs(definition) do + if is_column(col) then + c[i], c[col.name] = col, col; + elseif is_index(col) then + col.table = definition.name; + end + end + return setmetatable({ __table__ = definition, c = c, name = definition.name }, table_mt); +end +local function Index(definition) + return setmetatable(definition, index_mt); +end + +function table_mt:__tostring() + local s = { 'name="'..self.__table__.name..'"' } + for _, col in ipairs(self.__table__) do + s[#s+1] = tostring(col); + end + return 'Table{ '..t_concat(s, ", ")..' }' +end +table_mt.__index = {}; +function table_mt.__index:create(engine) + return engine:_create_table(self); +end +function column_mt:__tostring() + return 'Column{ name="'..self.name..'", type="'..self.type..'" }' +end +function index_mt:__tostring() + local s = 'Index{ name="'..self.name..'"'; + for i=1,#self do s = s..', "'..self[i]:gsub("[\\\"]", "\\%1")..'"'; end + return s..' }'; +-- return 'Index{ name="'..self.name..'", type="'..self.type..'" }' +end + +local engine = {}; +function engine:connect() + if self.conn then return true; end + + local params = self.params; + assert(params.driver == "SQLite3", "Only sqlite3 is supported"); + local dbh, err = sqlite_errors.coerce(lsqlite3.open(params.database)); + if not dbh then return nil, err; end + self.conn = dbh; + self.prepared = {}; + local ok, err = self:set_encoding(); + if not ok then + return ok, err; + end + local ok, err = self:onconnect(); + if ok == false then + return ok, err; + end + return true; +end +function engine:onconnect() -- luacheck: ignore 212/self + -- Override from create_engine() +end +function engine:ondisconnect() -- luacheck: ignore 212/self + -- Override from create_engine() +end + +function engine:execute(sql, ...) + local success, err = self:connect(); + if not success then return success, err; end + + if select('#', ...) == 0 then + local ret = self.conn:exec(sql); + if ret ~= lsqlite3.OK then + local err = sqlite_errors.new(err); + err.text = self.conn:errmsg(); + return err; + end + return true; + end + + local stmt, err = self.conn:prepare(sql); + if not stmt then + err = sqlite_errors.new(err); + err.text = self.conn:errmsg(); + return stmt, err; + end + + local ret = stmt:bind_values(...); + if ret ~= lsqlite3.OK then + return nil, sqlite_errors.new(ret, { message = self.conn:errmsg() }); + end + return stmt; +end + +local function iterator(table) + local i = 0; + return function() + i = i + 1; + local item = table[i]; + if item ~= nil then + return item; + end + end +end + +local result_mt = { + __len = function(self) + return self.__rowcount; + end; + __index = { + affected = function(self) + return self.__affected; + end; + rowcount = function(self) + return self.__rowcount; + end; + }; + __call = function(self) + return iterator(self.__data); + end; +}; + +local function debugquery(where, sql, ...) + local i = 0; local a = {...} + sql = sql:gsub("\n?\t+", " "); + log("debug", "[%s] %s", where, (sql:gsub("%?", function () + i = i + 1; + local v = a[i]; + if type(v) == "string" then + v = ("'%s'"):format(v:gsub("'", "''")); + end + return tostring(v); + end))); +end + +function engine:execute_update(sql, ...) + local prepared = self.prepared; + local stmt = prepared[sql]; + if stmt and stmt:isopen() then + prepared[sql] = nil; -- Can't be used concurrently + else + stmt = assert(self.conn:prepare(sql)); + end + local ret = stmt:bind_values(...); + if ret ~= lsqlite3.OK then error(self.conn:errmsg()); end + local data = array(); + for row in stmt:rows() do + data:push(array(row)); + end + -- FIXME Error handling, BUSY, ERROR, MISUSE + if stmt:reset() == lsqlite3.OK then + prepared[sql] = stmt; + end + local affected = self.conn:changes(); + return setmetatable({ __affected = affected; __rowcount = #data; __data = data }, result_mt); +end + +function engine:execute_query(sql, ...) + return self:execute_update(sql, ...)() +end + +engine.insert = engine.execute_update; +engine.select = engine.execute_query; +engine.delete = engine.execute_update; +engine.update = engine.execute_update; +local function debugwrap(name, f) + return function (self, sql, ...) + debugquery(name, sql, ...) + return f(self, sql, ...) + end +end +function engine:debug(enable) + self._debug = enable; + if enable then + engine.insert = debugwrap("insert", engine.execute_update); + engine.select = debugwrap("select", engine.execute_query); + engine.delete = debugwrap("delete", engine.execute_update); + engine.update = debugwrap("update", engine.execute_update); + else + engine.insert = engine.execute_update; + engine.select = engine.execute_query; + engine.delete = engine.execute_update; + engine.update = engine.execute_update; + end +end +function engine:_(word) + local ret = self.conn:exec(word); + if ret ~= lsqlite3.OK then return nil, self.conn:errmsg(); end + return true; +end +function engine:_transaction(func, ...) + if not self.conn then + local a,b = self:connect(); + if not a then return a,b; end + end + --assert(not self.__transaction, "Recursive transactions not allowed"); + local ok, err = self:_"BEGIN"; + if not ok then return ok, err; end + self.__transaction = true; + local success, a, b, c = xpcall(func, debug_traceback, ...); + self.__transaction = nil; + if success then + log("debug", "SQL transaction success [%s]", tostring(func)); + local ok, err = self:_"COMMIT"; + if not ok then return ok, err; end -- commit failed + return success, a, b, c; + else + log("debug", "SQL transaction failure [%s]: %s", tostring(func), a); + if self.conn then self:_"ROLLBACK"; end + return success, a; + end +end +function engine:transaction(...) + local ok, ret = self:_transaction(...); + if not ok then + local conn = self.conn; + if not conn or not conn:isopen() then + self.conn = nil; + self:ondisconnect(); + ok, ret = self:_transaction(...); + end + end + return ok, ret; +end +function engine:_create_index(index) + local sql = "CREATE INDEX IF NOT EXISTS \""..index.name.."\" ON \""..index.table.."\" ("; + for i=1,#index do + sql = sql.."\""..index[i].."\""; + if i ~= #index then sql = sql..", "; end + end + sql = sql..");" + if index.unique then + sql = sql:gsub("^CREATE", "CREATE UNIQUE"); + end + if self._debug then + debugquery("create", sql); + end + return self:execute(sql); +end +function engine:_create_table(table) + local sql = "CREATE TABLE IF NOT EXISTS \""..table.name.."\" ("; + for i,col in ipairs(table.c) do + local col_type = col.type; + sql = sql.."\""..col.name.."\" "..col_type; + if col.nullable == false then sql = sql.." NOT NULL"; end + if col.primary_key == true then sql = sql.." PRIMARY KEY"; end + if col.auto_increment == true then + sql = sql.." AUTOINCREMENT"; + end + if i ~= #table.c then sql = sql..", "; end + end + sql = sql.. ");" + if self._debug then + debugquery("create", sql); + end + local success,err = self:execute(sql); + if not success then return success,err; end + for _, v in ipairs(table.__table__) do + if is_index(v) then + self:_create_index(v); + end + end + return success; +end + +function engine:set_encoding() -- to UTF-8 + return self:transaction(function() + for encoding in self:select "PRAGMA encoding;" do + if encoding[1] == "UTF-8" then + self.charset = "utf8"; + end + end + end); +end +local engine_mt = { __index = engine }; + +local function db2uri(params) + return build_url{ + scheme = params.driver, + user = params.username, + password = params.password, + host = params.host, + port = params.port, + path = params.database, + }; +end + +local function create_engine(_, params, onconnect, ondisconnect) + assert(params.driver == "SQLite3", "Only SQLite3 is supported without LuaDBI"); + return setmetatable({ url = db2uri(params); params = params; onconnect = onconnect; ondisconnect = ondisconnect }, engine_mt); +end + +return { + is_column = is_column; + is_index = is_index; + is_table = is_table; + is_query = is_query; + Column = Column; + Table = Table; + Index = Index; + create_engine = create_engine; + db2uri = db2uri; +}; diff -r 54a936345aaa -r 5d9ec2e55d74 util/sslconfig.lua --- a/util/sslconfig.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/sslconfig.lua Wed Mar 27 15:39:03 2024 +0000 @@ -3,9 +3,12 @@ local type = type; local pairs = pairs; local rawset = rawset; +local rawget = rawget; +local error = error; local t_concat = table.concat; local t_insert = table.insert; local setmetatable = setmetatable; +local resolve_path = require"prosody.util.paths".resolve_relative_path; local _ENV = nil; -- luacheck: std none @@ -34,7 +37,7 @@ options[value] = true; end end - config[field] = options; + rawset(config, field, options) end handlers.verifyext = handlers.options; @@ -70,6 +73,20 @@ -- TLS 1.3 ciphers finalisers.ciphersuites = finalisers.ciphers; +-- Path expansion +function finalisers.key(path, config) + if type(path) == "string" then + return resolve_path(config._basedir, path); + else + return nil + end +end +finalisers.certificate = finalisers.key; +finalisers.cafile = finalisers.key; +finalisers.capath = finalisers.key; +-- XXX: copied from core/certmanager.lua, but this seems odd, because it would remove a dhparam function from the config +finalisers.dhparam = finalisers.key; + -- protocol = "x" should enable only that protocol -- protocol = "x+" should enable x and later versions @@ -89,37 +106,81 @@ -- Merge options from 'new' config into 'config' local function apply(config, new) + rawset(config, "_cache", nil); if type(new) == "table" then for field, value in pairs(new) do - (handlers[field] or rawset)(config, field, value); + -- exclude keys which are internal to the config builder + if field:sub(1, 1) ~= "_" then + (handlers[field] or rawset)(config, field, value); + end end end + return config end -- Finalize the config into the form LuaSec expects local function final(config) local output = { }; for field, value in pairs(config) do - output[field] = (finalisers[field] or id)(value); + -- exclude keys which are internal to the config builder + if field:sub(1, 1) ~= "_" then + output[field] = (finalisers[field] or id)(value, config); + end end -- Need to handle protocols last because it adds to the options list protocol(output); return output; end +local function build(config) + local cached = rawget(config, "_cache"); + if cached then + return cached, nil + end + + local ctx, err = rawget(config, "_context_factory")(config:final(), config); + if ctx then + rawset(config, "_cache", ctx); + end + return ctx, err +end + local sslopts_mt = { __index = { apply = apply; final = final; + build = build; }; + __newindex = function() + error("SSL config objects cannot be modified directly. Use :apply()") + end; }; -local function new() - return setmetatable({options={}}, sslopts_mt); + +-- passing basedir through everything is required to avoid sslconfig depending +-- on prosody.paths.config +local function new(context_factory, basedir) + return setmetatable({ + _context_factory = context_factory, + _basedir = basedir, + options={}, + }, sslopts_mt); end +local function clone(config) + local result = new(); + for k, v in pairs(config) do + -- note that we *do* copy the internal keys on clone -- we have to carry + -- both the factory and the cache with us + rawset(result, k, v); + end + return result +end + +sslopts_mt.__index.clone = clone; + return { apply = apply; final = final; - new = new; + _new = new; }; diff -r 54a936345aaa -r 5d9ec2e55d74 util/stanza.lua --- a/util/stanza.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/stanza.lua Wed Mar 27 15:39:03 2024 +0000 @@ -21,12 +21,15 @@ local s_gsub = string.gsub; local s_sub = string.sub; local s_find = string.find; +local t_move = table.move or require "prosody.util.table".move; +local t_create = require"prosody.util.table".create; -local valid_utf8 = require "util.encodings".utf8.valid; +local valid_utf8 = require "prosody.util.encodings".utf8.valid; -local do_pretty_printing, termcolours = pcall(require, "util.termcolours"); +local do_pretty_printing, termcolours = pcall(require, "prosody.util.termcolours"); local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"; +local xmpp_stanzas_attr = { xmlns = xmlns_stanzas }; local _ENV = nil; -- luacheck: std none @@ -179,6 +182,14 @@ return nil; end +function stanza_mt:get_child_attr(name, xmlns, attr) + local tag = self:get_child(name, xmlns); + if tag then + return tag.attr[attr]; + end + return nil; +end + function stanza_mt:child_with_name(name) for _, child in ipairs(self.tags) do if child.name == name then return child; end @@ -283,25 +294,33 @@ end local function _clone(stanza, only_top) - local attr, tags = {}, {}; + local attr = {}; for k,v in pairs(stanza.attr) do attr[k] = v; end local old_namespaces, namespaces = stanza.namespaces; if old_namespaces then namespaces = {}; for k,v in pairs(old_namespaces) do namespaces[k] = v; end end - local new = { name = stanza.name, attr = attr, namespaces = namespaces, tags = tags }; + local tags, new; + if only_top then + tags = {}; + new = { name = stanza.name, attr = attr, namespaces = namespaces, tags = tags }; + else + tags = t_create(#stanza.tags, 0); + new = t_create(#stanza, 4); + new.name = stanza.name; + new.attr = attr; + new.namespaces = namespaces; + new.tags = tags; + end + + setmetatable(new, stanza_mt); if not only_top then - for i=1,#stanza do - local child = stanza[i]; - if child.name then - child = _clone(child); - t_insert(tags, child); - end - t_insert(new, child); - end + t_move(stanza, 1, #stanza, 1, new); + t_move(stanza.tags, 1, #stanza.tags, 1, tags); + new:maptags(_clone); end - return setmetatable(new, stanza_mt); + return new; end local function clone(stanza, only_top) @@ -387,6 +406,33 @@ return error_type, condition or "undefined-condition", text, extra_tag; end +function stanza_mt.add_error(stanza, error_type, condition, error_message, error_by) + local extra; + if type(error_type) == "table" then -- an util.error or similar object + if type(error_type.extra) == "table" then + extra = error_type.extra; + end + if type(error_type.context) == "table" and type(error_type.context.by) == "string" then error_by = error_type.context.by; end + error_type, condition, error_message = error_type.type, error_type.condition, error_type.text; + end + if stanza.attr.from == error_by then + error_by = nil; + end + stanza:tag("error", {type = error_type, by = error_by}) --COMPAT: Some day xmlns:stanzas goes here + :tag(condition, xmpp_stanzas_attr); + if extra and condition == "gone" and type(extra.uri) == "string" then + stanza:text(extra.uri); + end + stanza:up(); + if error_message then stanza:text_tag("text", error_message, xmpp_stanzas_attr); end + if extra and is_stanza(extra.tag) then + stanza:add_child(extra.tag); + elseif extra and extra.namespace and extra.condition then + stanza:tag(extra.condition, { xmlns = extra.namespace }):up(); + end + return stanza:up(); +end + local function preserialize(stanza) local s = { name = stanza.name, attr = stanza.attr }; for _, child in ipairs(stanza) do @@ -461,7 +507,6 @@ }); end -local xmpp_stanzas_attr = { xmlns = xmlns_stanzas }; local function error_reply(orig, error_type, condition, error_message, error_by) if not is_stanza(orig) then error("bad argument to error_reply: expected stanza, got "..type(orig)); @@ -470,30 +515,9 @@ end local t = reply(orig); t.attr.type = "error"; - local extra; - if type(error_type) == "table" then -- an util.error or similar object - if type(error_type.extra) == "table" then - extra = error_type.extra; - end - if type(error_type.context) == "table" and type(error_type.context.by) == "string" then error_by = error_type.context.by; end - error_type, condition, error_message = error_type.type, error_type.condition, error_type.text; - end - if t.attr.from == error_by then - error_by = nil; - end - t:tag("error", {type = error_type, by = error_by}) --COMPAT: Some day xmlns:stanzas goes here - :tag(condition, xmpp_stanzas_attr); - if extra and condition == "gone" and type(extra.uri) == "string" then - t:text(extra.uri); - end - t:up(); - if error_message then t:text_tag("text", error_message, xmpp_stanzas_attr); end - if extra and is_stanza(extra.tag) then - t:add_child(extra.tag); - elseif extra and extra.namespace and extra.condition then - t:tag(extra.condition, { xmlns = extra.namespace }):up(); - end - return t; -- stanza ready for adding app-specific errors + t:add_error(error_type, condition, error_message, error_by); + t.last_add = { t[1] }; -- ready to add application-specific errors + return t; end local function presence(attr) diff -r 54a936345aaa -r 5d9ec2e55d74 util/startup.lua --- a/util/startup.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/startup.lua Wed Mar 27 15:39:03 2024 +0000 @@ -2,15 +2,15 @@ -- luacheck: ignore 113/CFG_CONFIGDIR 113/CFG_SOURCEDIR 113/CFG_DATADIR 113/CFG_PLUGINDIR local startup = {}; -local prosody = { events = require "util.events".new() }; -local logger = require "util.logger"; +local prosody = { events = require "prosody.util.events".new() }; +local logger = require "prosody.util.logger"; local log = logger.init("startup"); -local parse_args = require "util.argparse".parse; +local parse_args = require "prosody.util.argparse".parse; -local config = require "core.configmanager"; +local config = require "prosody.core.configmanager"; local config_warnings; -local dependencies = require "util.dependencies"; +local dependencies = require "prosody.util.dependencies"; local original_logging_config; @@ -132,14 +132,14 @@ function startup.load_libraries() -- Load socket framework -- luacheck: ignore 111/server 111/socket - require "util.import" + require "prosody.util.import" socket = require "socket"; - server = require "net.server" + server = require "prosody.net.server" end function startup.init_logging() -- Initialize logging - local loggingmanager = require "core.loggingmanager" + local loggingmanager = require "prosody.core.loggingmanager" loggingmanager.reload_logging(); prosody.events.add_handler("config-reloaded", function () prosody.events.fire_event("reopen-log-files"); @@ -233,7 +233,7 @@ if info.isvararg then info[n_params+1] = "..."; end - return ("function<%s:%d>(%s)"):format(info.short_src:match("[^\\/]*$"), info.linedefined, table.concat(info, ", ")); + return ("function @%s:%d(%s)"):format(info.short_src:match("[^\\/]*$"), info.linedefined, table.concat(info, ", ")); end debug.setmetatable(function() end, mt); end @@ -277,6 +277,11 @@ startup.detect_platform(); startup.detect_installed(); _G.prosody = prosody; + + -- COMPAT Lua < 5.3 + if not math.type then + require "prosody.util.mathcompat" + end end function startup.setup_datadir() @@ -298,7 +303,7 @@ local installer_plugin_path = config.get("*", "installer_plugin_path") or "custom_plugins"; local path_sep = package.config:sub(3,3); installer_plugin_path = config.resolve_relative_path(CFG_DATADIR or "data", installer_plugin_path); - require"util.paths".complement_lua_path(installer_plugin_path); + require"prosody.util.paths".complement_lua_path(installer_plugin_path); -- luacheck: ignore 111 CFG_PLUGINDIR = installer_plugin_path..path_sep..(CFG_PLUGINDIR or "plugins"); prosody.paths.installer = installer_plugin_path; @@ -360,28 +365,23 @@ function startup.load_secondary_libraries() --- Load and initialise core modules - require "util.xmppstream" - require "core.stanza_router" - require "core.statsmanager" - require "core.hostmanager" - require "core.portmanager" - require "core.modulemanager" - require "core.usermanager" - require "core.rostermanager" - require "core.sessionmanager" - package.loaded['core.componentmanager'] = setmetatable({},{__index=function() - -- COMPAT which version? - log("warn", "componentmanager is deprecated: %s", debug.traceback():match("\n[^\n]*\n[ \t]*([^\n]*)")); - return function() end - end}); + require "prosody.util.xmppstream" + require "prosody.core.stanza_router" + require "prosody.core.statsmanager".metric("gauge", "prosody_info", "", "Prosody version", { "version" }):with_labels(prosody.version):set(1); + require "prosody.core.hostmanager" + require "prosody.core.portmanager" + require "prosody.core.modulemanager" + require "prosody.core.usermanager" + require "prosody.core.rostermanager" + require "prosody.core.sessionmanager" - require "util.array" - require "util.datetime" - require "util.iterators" - require "util.timer" - require "util.helpers" + require "prosody.util.array" + require "prosody.util.datetime" + require "prosody.util.iterators" + require "prosody.util.timer" + require "prosody.util.helpers" - pcall(require, "util.signal") -- Not on Windows + pcall(require, "prosody.util.signal") -- Not on Windows -- Commented to protect us from -- the second kind of people @@ -390,43 +390,83 @@ if remdebug then remdebug.engine.start() end ]] - require "util.stanza" - require "util.jid" + require "prosody.util.stanza" + require "prosody.util.jid" + + prosody.features = require "prosody.core.features".available; end function startup.init_http_client() - local http = require "net.http" + local http = require "prosody.net.http" local config_ssl = config.get("*", "ssl") or {} local https_client = config.get("*", "client_https_ssl") - http.default.options.sslctx = require "core.certmanager".create_context("client_https port 0", "client", + http.default.options.sslctx = require "prosody.core.certmanager".create_context("client_https port 0", "client", { capath = config_ssl.capath, cafile = config_ssl.cafile, verify = "peer", }, https_client); http.default.options.use_dane = config.get("*", "use_dane") end function startup.init_promise() - local promise = require "util.promise"; + local promise = require "prosody.util.promise"; - local timer = require "util.timer"; + local timer = require "prosody.util.timer"; promise.set_nexttick(function(f) return timer.add_task(0, f); end); end function startup.init_async() - local async = require "util.async"; + local async = require "prosody.util.async"; - local timer = require "util.timer"; + local timer = require "prosody.util.timer"; async.set_nexttick(function(f) return timer.add_task(0, f); end); async.set_schedule_function(timer.add_task); end function startup.init_data_store() - require "core.storagemanager"; + require "prosody.core.storagemanager"; end +local running_state = require "prosody.util.fsm".new({ + default_state = "uninitialized"; + transitions = { + { name = "begin_startup", from = "uninitialized", to = "starting" }; + { name = "finish_startup", from = "starting", to = "running" }; + { name = "begin_shutdown", from = { "running", "starting" }, to = "stopping" }; + { name = "finish_shutdown", from = "stopping", to = "stopped" }; + }; + handlers = { + transitioned = function (transition) + prosody.state = transition.to; + end; + }; + state_handlers = { + starting = function () + prosody.log("debug", "Firing server-starting event"); + prosody.events.fire_event("server-starting"); + prosody.start_time = os.time(); + end; + running = function () + prosody.log("debug", "Startup complete, firing server-started"); + prosody.events.fire_event("server-started"); + end; + }; +}):init(); + function startup.prepare_to_start() log("info", "Prosody is using the %s backend for connection handling", server.get_backend()); + -- Signal to modules that we are ready to start - prosody.events.fire_event("server-starting"); - prosody.start_time = os.time(); + prosody.started = require "prosody.util.promise".new(function (resolve) + if prosody.state == "running" then + resolve(); + else + prosody.events.add_handler("server-started", function () + resolve(); + end); + end + end):catch(function (err) + prosody.log("error", "Prosody startup error: %s", err); + end); + + running_state:begin_startup(); end function startup.init_global_protection() @@ -460,7 +500,7 @@ prosody.version = "hg:"..prosody.version; end else - local hg = require"util.mercurial"; + local hg = require"prosody.util.mercurial"; local hgid = hg.check_id(CFG_SOURCEDIR or "."); if hgid then prosody.version = "hg:" .. hgid; end end @@ -471,7 +511,7 @@ end function startup.notify_started() - prosody.events.fire_event("server-started"); + running_state:finish_startup(); end -- Override logging config (used by prosodyctl) @@ -491,21 +531,30 @@ config.set("*", "log", { { levels = { min = log_level or "info" }, to = "console" } }); end +local function check_posix() + if prosody.platform ~= "posix" then return end + + local want_pposix_version = "0.4.0"; + local have_pposix, pposix = pcall(require, "prosody.util.pposix"); + + if pposix._VERSION ~= want_pposix_version then + print(string.format("Unknown version (%s) of binary pposix module, expected %s", + tostring(pposix._VERSION), want_pposix_version)); + os.exit(1); + end + if have_pposix and pposix then + return pposix; + end +end + function startup.switch_user() -- Switch away from root and into the prosody user -- -- NOTE: This function is only used by prosodyctl. -- The prosody process is built with the assumption that -- it is already started as the appropriate user. - local want_pposix_version = "0.4.0"; - local have_pposix, pposix = pcall(require, "util.pposix"); - - if have_pposix and pposix then - if pposix._VERSION ~= want_pposix_version then - print(string.format("Unknown version (%s) of binary pposix module, expected %s", - tostring(pposix._VERSION), want_pposix_version)); - os.exit(1); - end + local pposix = check_posix() + if pposix then prosody.current_uid = pposix.getuid(); local arg_root = prosody.opts.root; if prosody.current_uid == 0 and config.get("*", "run_as_root") ~= true and not arg_root then @@ -594,7 +643,7 @@ function startup.init_gc() -- Apply garbage collector settings from the config file - local gc = require "util.gc"; + local gc = require "prosody.util.gc"; local gc_settings = config.get("*", "gc") or { mode = default_gc_params.mode }; local ok, err = gc.configure(gc_settings, default_gc_params); @@ -606,7 +655,7 @@ end function startup.init_errors() - require "util.error".configure(config.get("*", "error_library") or {}); + require "prosody.util.error".configure(config.get("*", "error_library") or {}); end function startup.make_host(hostname) @@ -615,7 +664,7 @@ events = prosody.events, modules = {}, sessions = {}, - users = require "core.usermanager".new_null_provider(hostname) + users = require "prosody.core.usermanager".new_null_provider(hostname) }; end @@ -631,17 +680,150 @@ end end +function startup.posix_umask() + if prosody.platform ~= "posix" then return end + local pposix = require "prosody.util.pposix"; + local umask = config.get("*", "umask") or "027"; + pposix.umask(umask); +end + +function startup.check_user() + local pposix = check_posix(); + if not pposix then return end + -- Don't even think about it! + if pposix.getuid() == 0 and not config.get("*", "run_as_root") then + print("Danger, Will Robinson! Prosody doesn't need to be run as root, so don't do it!"); + print("For more information on running Prosody as root, see https://prosody.im/doc/root"); + os.exit(1); -- Refusing to run as root + end +end + +local function remove_pidfile() + local pidfile = prosody.pidfile; + if prosody.pidfile_handle then + prosody.pidfile_handle:close(); + os.remove(pidfile); + prosody.pidfile, prosody.pidfile_handle = nil, nil; + end +end + +function startup.write_pidfile() + local pposix = check_posix(); + if not pposix then return end + local lfs = require "lfs"; + local stat = lfs.attributes; + local pidfile = config.get("*", "pidfile") or nil; + if not pidfile then return end + pidfile = config.resolve_relative_path(prosody.paths.data, pidfile); + local mode = stat(pidfile) and "r+" or "w+"; + local pidfile_handle, err = io.open(pidfile, mode); + if not pidfile_handle then + log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + prosody.shutdown("Couldn't write pidfile", 1); + else + prosody.pidfile = pidfile; + if not lfs.lock(pidfile_handle, "w") then -- Exclusive lock + local other_pid = pidfile_handle:read("*a"); + log("error", "Another Prosody instance seems to be running with PID %s, quitting", other_pid); + prosody.pidfile_handle = nil; + prosody.shutdown("Prosody already running", 1); + else + pidfile_handle:close(); + pidfile_handle, err = io.open(pidfile, "w+"); + if not pidfile_handle then + log("error", "Couldn't write pidfile at %s; %s", pidfile, err); + prosody.shutdown("Couldn't write pidfile", 1); + else + if lfs.lock(pidfile_handle, "w") then + pidfile_handle:write(tostring(pposix.getpid())); + pidfile_handle:flush(); + prosody.pidfile_handle = pidfile_handle; + end + end + end + end + prosody.events.add_handler("server-stopped", remove_pidfile); +end + +local function remove_log_sinks() + local lm = require "prosody.core.loggingmanager"; + lm.register_sink_type("console", nil); + lm.register_sink_type("stdout", nil); + lm.reload_logging(); +end + +function startup.posix_daemonize() + if not prosody.opts.daemonize then return end + local pposix = check_posix(); + log("info", "Prosody is about to detach from the console, disabling further console output"); + remove_log_sinks(); + local ok, ret = pposix.daemonize(); + if not ok then + log("error", "Failed to daemonize: %s", ret); + elseif ret and ret > 0 then + os.exit(0); + else + log("info", "Successfully daemonized to PID %d", pposix.getpid()); + end +end + +function startup.hook_posix_signals() + if prosody.platform ~= "posix" then return end + local have_signal, signal = pcall(require, "prosody.util.signal"); + if not have_signal then + log("warn", "Couldn't load signal library, won't respond to SIGTERM"); + return + end + signal.signal("SIGTERM", function() + log("warn", "Received SIGTERM"); + prosody.main_thread:run(function() + prosody.unlock_globals(); + prosody.shutdown("Received SIGTERM"); + prosody.lock_globals(); + end); + end); + + signal.signal("SIGHUP", function() + log("info", "Received SIGHUP"); + prosody.main_thread:run(function() prosody.reload_config(); end); + -- this also reloads logging + end); + + signal.signal("SIGINT", function() + log("info", "Received SIGINT"); + prosody.main_thread:run(function() + prosody.unlock_globals(); + prosody.shutdown("Received SIGINT"); + prosody.lock_globals(); + end); + end); + + signal.signal("SIGUSR1", function() + log("info", "Received SIGUSR1"); + prosody.events.fire_event("signal/SIGUSR1"); + end); + + signal.signal("SIGUSR2", function() + log("info", "Received SIGUSR2"); + prosody.events.fire_event("signal/SIGUSR2"); + end); +end + function startup.cleanup() prosody.log("info", "Shutdown status: Cleaning up"); prosody.events.fire_event("server-cleanup"); end function startup.shutdown() + running_state:begin_shutdown(); + prosody.log("info", "Shutting down..."); startup.cleanup(); prosody.events.fire_event("server-stopped"); + + running_state:finish_shutdown(); + prosody.log("info", "Shutdown complete"); - prosody.log("debug", "Shutdown reason was: %s", prosody.shutdown_reason or "not specified"); prosody.log("debug", "Exiting with status code: %d", prosody.shutdown_code or 0); server.setquitting(true); @@ -682,6 +864,7 @@ startup.parse_args(); startup.init_global_state(); startup.read_config(); + startup.check_user(); startup.init_logging(); startup.init_gc(); startup.init_errors(); @@ -704,6 +887,9 @@ startup.init_http_client(); startup.init_data_store(); startup.init_global_protection(); + startup.posix_daemonize(); + startup.write_pidfile(); + startup.hook_posix_signals(); startup.prepare_to_start(); startup.notify_started(); end diff -r 54a936345aaa -r 5d9ec2e55d74 util/statistics.lua --- a/util/statistics.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/statistics.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,6 @@ -local time = require "util.time".now; -local new_metric_registry = require "util.openmetrics".new_metric_registry; -local render_histogram_le = require "util.openmetrics".render_histogram_le; +local time = require "prosody.util.time".now; +local new_metric_registry = require "prosody.util.openmetrics".new_metric_registry; +local render_histogram_le = require "prosody.util.openmetrics".render_histogram_le; -- BEGIN of Metric implementations diff -r 54a936345aaa -r 5d9ec2e55d74 util/statsd.lua --- a/util/statsd.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/statsd.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,10 +1,10 @@ local socket = require "socket"; -local time = require "util.time".now; -local array = require "util.array"; +local time = require "prosody.util.time".now; +local array = require "prosody.util.array"; local t_concat = table.concat; -local new_metric_registry = require "util.openmetrics".new_metric_registry; -local render_histogram_le = require "util.openmetrics".render_histogram_le; +local new_metric_registry = require "prosody.util.openmetrics".new_metric_registry; +local render_histogram_le = require "prosody.util.openmetrics".render_histogram_le; -- BEGIN of Metric implementations diff -r 54a936345aaa -r 5d9ec2e55d74 util/template.lua --- a/util/template.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/template.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,13 +1,13 @@ -- luacheck: ignore 213/i -local stanza_mt = require "util.stanza".stanza_mt; +local stanza_mt = require "prosody.util.stanza".stanza_mt; local setmetatable = setmetatable; local pairs = pairs; local ipairs = ipairs; local error = error; -local envload = require "util.envload".envload; +local envload = require "prosody.util.envload".envload; local debug = debug; local t_remove = table.remove; -local parse_xml = require "util.xml".parse; +local parse_xml = require "prosody.util.xml".parse; local _ENV = nil; -- luacheck: std none diff -r 54a936345aaa -r 5d9ec2e55d74 util/termcolours.lua --- a/util/termcolours.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/termcolours.lua Wed Mar 27 15:39:03 2024 +0000 @@ -21,7 +21,7 @@ local windows; if os.getenv("WINDIR") then - windows = require "util.windows"; + windows = require "prosody.util.windows"; end local orig_color = windows and windows.get_consolecolor and windows.get_consolecolor(); diff -r 54a936345aaa -r 5d9ec2e55d74 util/throttle.lua --- a/util/throttle.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/throttle.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,5 +1,5 @@ -local gettime = require "util.time".now +local gettime = require "prosody.util.time".now local setmetatable = setmetatable; local _ENV = nil; diff -r 54a936345aaa -r 5d9ec2e55d74 util/timer.lua --- a/util/timer.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/timer.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,14 +6,14 @@ -- COPYING file in the source package for more information. -- -local indexedbheap = require "util.indexedbheap"; -local log = require "util.logger".init("timer"); -local server = require "net.server"; -local get_time = require "util.time".now +local indexedbheap = require "prosody.util.indexedbheap"; +local log = require "prosody.util.logger".init("timer"); +local server = require "prosody.net.server"; +local get_time = require "prosody.util.time".now local type = type; local debug_traceback = debug.traceback; local tostring = tostring; -local xpcall = require "util.xpcall".xpcall; +local xpcall = require "prosody.util.xpcall".xpcall; local math_max = math.max; local pairs = pairs; diff -r 54a936345aaa -r 5d9ec2e55d74 util/uuid.lua --- a/util/uuid.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/uuid.lua Wed Mar 27 15:39:03 2024 +0000 @@ -6,10 +6,12 @@ -- COPYING file in the source package for more information. -- -local random = require "util.random"; +local random = require "prosody.util.random"; local random_bytes = random.bytes; -local hex = require "util.hex".encode; +local time = require "prosody.util.time"; +local hex = require "prosody.util.hex".encode; local m_ceil = math.ceil; +local m_floor = math.floor; local function get_nibbles(n) return hex(random_bytes(m_ceil(n/2))):sub(1, n); @@ -24,7 +26,22 @@ return get_nibbles(8).."-"..get_nibbles(4).."-4"..get_nibbles(3).."-"..(get_twobits())..get_nibbles(3).."-"..get_nibbles(12); end +local function generate_v7() + -- Sortable based on time and random + -- https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-01#section-4.4 + local t = time.now(); + local unixts = m_floor(t); + local unixts_a = m_floor(unixts / 16); + local unixts_b = m_floor(unixts % 16); + local subsec = t % 1; + local subsec_a = m_floor(subsec * 0x1000); + local subsec_b = m_floor(subsec * 0x1000000) % 0x1000; + return ("%08x-%x%03x-7%03x-%4s-%12s"):format(unixts_a, unixts_b, subsec_a, subsec_b, get_twobits() .. get_nibbles(3), get_nibbles(12)); +end + return { + v4 = generate; + v7 = generate_v7; get_nibbles=get_nibbles; generate = generate ; -- COMPAT diff -r 54a936345aaa -r 5d9ec2e55d74 util/vcard.lua --- a/util/vcard.lua Wed Mar 27 15:35:15 2024 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,574 +0,0 @@ --- Copyright (C) 2011-2014 Kim Alvefur --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - --- TODO --- Fix folding. - -local st = require "util.stanza"; -local t_insert, t_concat = table.insert, table.concat; -local type = type; -local pairs, ipairs = pairs, ipairs; - -local from_text, to_text, from_xep54, to_xep54; - -local line_sep = "\n"; - -local vCard_dtd; -- See end of file -local vCard4_dtd; - -local function vCard_esc(s) - return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n"); -end - -local function vCard_unesc(s) - return s:gsub("\\?[\\nt:;,]", { - ["\\\\"] = "\\", - ["\\n"] = "\n", - ["\\r"] = "\r", - ["\\t"] = "\t", - ["\\:"] = ":", -- FIXME Shouldn't need to escape : in values, just params - ["\\;"] = ";", - ["\\,"] = ",", - [":"] = "\29", - [";"] = "\30", - [","] = "\31", - }); -end - -local function item_to_xep54(item) - local t = st.stanza(item.name, { xmlns = "vcard-temp" }); - - local prop_def = vCard_dtd[item.name]; - if prop_def == "text" then - t:text(item[1]); - elseif type(prop_def) == "table" then - if prop_def.types and item.TYPE then - if type(item.TYPE) == "table" then - for _,v in pairs(prop_def.types) do - for _,typ in pairs(item.TYPE) do - if typ:upper() == v then - t:tag(v):up(); - break; - end - end - end - else - t:tag(item.TYPE:upper()):up(); - end - end - - if prop_def.props then - for _,prop in pairs(prop_def.props) do - if item[prop] then - for _, v in ipairs(item[prop]) do - t:text_tag(prop, v); - end - end - end - end - - if prop_def.value then - t:text_tag(prop_def.value, item[1]); - elseif prop_def.values then - local prop_def_values = prop_def.values; - local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values]; - for i=1,#item do - t:text_tag(prop_def.values[i] or repeat_last, item[i]); - end - end - end - - return t; -end - -local function vcard_to_xep54(vCard) - local t = st.stanza("vCard", { xmlns = "vcard-temp" }); - for i=1,#vCard do - t:add_child(item_to_xep54(vCard[i])); - end - return t; -end - -function to_xep54(vCards) - if not vCards[1] or vCards[1].name then - return vcard_to_xep54(vCards) - else - local t = st.stanza("xCard", { xmlns = "vcard-temp" }); - for i=1,#vCards do - t:add_child(vcard_to_xep54(vCards[i])); - end - return t; - end -end - -function from_text(data) - data = data -- unfold and remove empty lines - :gsub("\r\n","\n") - :gsub("\n ", "") - :gsub("\n\n+","\n"); - local vCards = {}; - local current; - for line in data:gmatch("[^\n]+") do - line = vCard_unesc(line); - local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); - value = value:gsub("\29",":"); - if #params > 0 then - local _params = {}; - for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do - k = k:upper(); - local _vt = {}; - for _p in v:gmatch("[^\31]+") do - _vt[#_vt+1]=_p - _vt[_p]=true; - end - if isval == "=" then - _params[k]=_vt; - else - _params[k]=true; - end - end - params = _params; - end - if name == "BEGIN" and value == "VCARD" then - current = {}; - vCards[#vCards+1] = current; - elseif name == "END" and value == "VCARD" then - current = nil; - elseif current and vCard_dtd[name] then - local dtd = vCard_dtd[name]; - local item = { name = name }; - t_insert(current, item); - local up = current; - current = item; - if dtd.types then - for _, t in ipairs(dtd.types) do - t = t:lower(); - if ( params.TYPE and params.TYPE[t] == true) - or params[t] == true then - current.TYPE=t; - end - end - end - if dtd.props then - for _, p in ipairs(dtd.props) do - if params[p] then - if params[p] == true then - current[p]=true; - else - for _, prop in ipairs(params[p]) do - current[p]=prop; - end - end - end - end - end - if dtd == "text" or dtd.value then - t_insert(current, value); - elseif dtd.values then - for p in ("\30"..value):gmatch("\30([^\30]*)") do - t_insert(current, p); - end - end - current = up; - end - end - return vCards; -end - -local function item_to_text(item) - local value = {}; - for i=1,#item do - value[i] = vCard_esc(item[i]); - end - value = t_concat(value, ";"); - - local params = ""; - for k,v in pairs(item) do - if type(k) == "string" and k ~= "name" then - params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v); - end - end - - return ("%s%s:%s"):format(item.name, params, value) -end - -local function vcard_to_text(vcard) - local t={}; - t_insert(t, "BEGIN:VCARD") - for i=1,#vcard do - t_insert(t, item_to_text(vcard[i])); - end - t_insert(t, "END:VCARD") - return t_concat(t, line_sep); -end - -function to_text(vCards) - if vCards[1] and vCards[1].name then - return vcard_to_text(vCards) - else - local t = {}; - for i=1,#vCards do - t[i]=vcard_to_text(vCards[i]); - end - return t_concat(t, line_sep); - end -end - -local function from_xep54_item(item) - local prop_name = item.name; - local prop_def = vCard_dtd[prop_name]; - - local prop = { name = prop_name }; - - if prop_def == "text" then - prop[1] = item:get_text(); - elseif type(prop_def) == "table" then - if prop_def.value then --single item - prop[1] = item:get_child_text(prop_def.value) or ""; - elseif prop_def.values then --array - local value_names = prop_def.values; - if value_names.behaviour == "repeat-last" then - for i=1,#item.tags do - t_insert(prop, item.tags[i]:get_text() or ""); - end - else - for i=1,#value_names do - t_insert(prop, item:get_child_text(value_names[i]) or ""); - end - end - elseif prop_def.names then - local names = prop_def.names; - for i=1,#names do - if item:get_child(names[i]) then - prop[1] = names[i]; - break; - end - end - end - - if prop_def.props_verbatim then - for k,v in pairs(prop_def.props_verbatim) do - prop[k] = v; - end - end - - if prop_def.types then - local types = prop_def.types; - prop.TYPE = {}; - for i=1,#types do - if item:get_child(types[i]) then - t_insert(prop.TYPE, types[i]:lower()); - end - end - if #prop.TYPE == 0 then - prop.TYPE = nil; - end - end - - -- A key-value pair, within a key-value pair? - if prop_def.props then - local params = prop_def.props; - for i=1,#params do - local name = params[i] - local data = item:get_child_text(name); - if data then - prop[name] = prop[name] or {}; - t_insert(prop[name], data); - end - end - end - else - return nil - end - - return prop; -end - -local function from_xep54_vCard(vCard) - local tags = vCard.tags; - local t = {}; - for i=1,#tags do - t_insert(t, from_xep54_item(tags[i])); - end - return t -end - -function from_xep54(vCard) - if vCard.attr.xmlns ~= "vcard-temp" then - return nil, "wrong-xmlns"; - end - if vCard.name == "xCard" then -- A collection of vCards - local t = {}; - local vCards = vCard.tags; - for i=1,#vCards do - t[i] = from_xep54_vCard(vCards[i]); - end - return t - elseif vCard.name == "vCard" then -- A single vCard - return from_xep54_vCard(vCard) - end -end - -local vcard4 = { } - -function vcard4:text(node, params, value) -- luacheck: ignore 212/params - self:tag(node:lower()) - -- FIXME params - if type(value) == "string" then - self:text_tag("text", value); - elseif vcard4[node] then - vcard4[node](value); - end - self:up(); -end - -function vcard4.N(value) - for i, k in ipairs(vCard_dtd.N.values) do - value:text_tag(k, value[i]); - end -end - -local xmlns_vcard4 = "urn:ietf:params:xml:ns:vcard-4.0" - -local function item_to_vcard4(item) - local typ = item.name:lower(); - local t = st.stanza(typ, { xmlns = xmlns_vcard4 }); - - local prop_def = vCard4_dtd[typ]; - if prop_def == "text" then - t:text_tag("text", item[1]); - elseif prop_def == "uri" then - if item.ENCODING and item.ENCODING[1] == 'b' then - t:text_tag("uri", "data:;base64," .. item[1]); - else - t:text_tag("uri", item[1]); - end - elseif type(prop_def) == "table" then - if prop_def.values then - for i, v in ipairs(prop_def.values) do - t:text_tag(v:lower(), item[i]); - end - else - t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"}) - end - else - t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"}) - end - return t; -end - -local function vcard_to_vcard4xml(vCard) - local t = st.stanza("vcard", { xmlns = xmlns_vcard4 }); - for i=1,#vCard do - t:add_child(item_to_vcard4(vCard[i])); - end - return t; -end - -local function vcards_to_vcard4xml(vCards) - if not vCards[1] or vCards[1].name then - return vcard_to_vcard4xml(vCards) - else - local t = st.stanza("vcards", { xmlns = xmlns_vcard4 }); - for i=1,#vCards do - t:add_child(vcard_to_vcard4xml(vCards[i])); - end - return t; - end -end - --- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd -vCard_dtd = { - VERSION = "text", --MUST be 3.0, so parsing is redundant - FN = "text", - N = { - values = { - "FAMILY", - "GIVEN", - "MIDDLE", - "PREFIX", - "SUFFIX", - }, - }, - NICKNAME = "text", - PHOTO = { - props_verbatim = { ENCODING = { "b" } }, - props = { "TYPE" }, - value = "BINVAL", --{ "EXTVAL", }, - }, - BDAY = "text", - ADR = { - types = { - "HOME", - "WORK", - "POSTAL", - "PARCEL", - "DOM", - "INTL", - "PREF", - }, - values = { - "POBOX", - "EXTADD", - "STREET", - "LOCALITY", - "REGION", - "PCODE", - "CTRY", - } - }, - LABEL = { - types = { - "HOME", - "WORK", - "POSTAL", - "PARCEL", - "DOM", - "INTL", - "PREF", - }, - value = "LINE", - }, - TEL = { - types = { - "HOME", - "WORK", - "VOICE", - "FAX", - "PAGER", - "MSG", - "CELL", - "VIDEO", - "BBS", - "MODEM", - "ISDN", - "PCS", - "PREF", - }, - value = "NUMBER", - }, - EMAIL = { - types = { - "HOME", - "WORK", - "INTERNET", - "PREF", - "X400", - }, - value = "USERID", - }, - JABBERID = "text", - MAILER = "text", - TZ = "text", - GEO = { - values = { - "LAT", - "LON", - }, - }, - TITLE = "text", - ROLE = "text", - LOGO = "copy of PHOTO", - AGENT = "text", - ORG = { - values = { - behaviour = "repeat-last", - "ORGNAME", - "ORGUNIT", - } - }, - CATEGORIES = { - values = "KEYWORD", - }, - NOTE = "text", - PRODID = "text", - REV = "text", - SORTSTRING = "text", - SOUND = "copy of PHOTO", - UID = "text", - URL = "text", - CLASS = { - names = { -- The item.name is the value if it's one of these. - "PUBLIC", - "PRIVATE", - "CONFIDENTIAL", - }, - }, - KEY = { - props = { "TYPE" }, - value = "CRED", - }, - DESC = "text", -}; -vCard_dtd.LOGO = vCard_dtd.PHOTO; -vCard_dtd.SOUND = vCard_dtd.PHOTO; - -vCard4_dtd = { - source = "uri", - kind = "text", - xml = "text", - fn = "text", - n = { - values = { - "family", - "given", - "middle", - "prefix", - "suffix", - }, - }, - nickname = "text", - photo = "uri", - bday = "date-and-or-time", - anniversary = "date-and-or-time", - gender = "text", - adr = { - values = { - "pobox", - "ext", - "street", - "locality", - "region", - "code", - "country", - } - }, - tel = "text", - email = "text", - impp = "uri", - lang = "language-tag", - tz = "text", - geo = "uri", - title = "text", - role = "text", - logo = "uri", - org = "text", - member = "uri", - related = "uri", - categories = "text", - note = "text", - prodid = "text", - rev = "timestamp", - sound = "uri", - uid = "uri", - clientpidmap = "number, uuid", - url = "uri", - version = "text", - key = "uri", - fburl = "uri", - caladruri = "uri", - caluri = "uri", -}; - -return { - from_text = from_text; - to_text = to_text; - - from_xep54 = from_xep54; - to_xep54 = to_xep54; - - to_vcard4 = vcards_to_vcard4xml; -}; diff -r 54a936345aaa -r 5d9ec2e55d74 util/watchdog.lua --- a/util/watchdog.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/watchdog.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,6 +1,5 @@ -local timer = require "util.timer"; +local timer = require "prosody.util.timer"; local setmetatable = setmetatable; -local os_time = os.time; local _ENV = nil; -- luacheck: std none @@ -9,27 +8,35 @@ local watchdog_mt = { __index = watchdog_methods }; local function new(timeout, callback) - local watchdog = setmetatable({ timeout = timeout, last_reset = os_time(), callback = callback }, watchdog_mt); - timer.add_task(timeout+1, function (current_time) - local last_reset = watchdog.last_reset; - if not last_reset then - return; - end - local time_left = (last_reset + timeout) - current_time; - if time_left < 0 then - return watchdog:callback(); - end - return time_left + 1; - end); + local watchdog = setmetatable({ + timeout = timeout; + callback = callback; + timer_id = nil; + }, watchdog_mt); + + watchdog:reset(); -- Kick things off + return watchdog; end -function watchdog_methods:reset() - self.last_reset = os_time(); +function watchdog_methods:reset(new_timeout) + if new_timeout then + self.timeout = new_timeout; + end + if self.timer_id then + timer.reschedule(self.timer_id, self.timeout+1); + else + self.timer_id = timer.add_task(self.timeout+1, function () + return self:callback(); + end); + end end function watchdog_methods:cancel() - self.last_reset = nil; + if self.timer_id then + timer.stop(self.timer_id); + self.timer_id = nil; + end end return { diff -r 54a936345aaa -r 5d9ec2e55d74 util/x509.lua --- a/util/x509.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/x509.lua Wed Mar 27 15:39:03 2024 +0000 @@ -11,19 +11,19 @@ -- IDN libraries complicate that. --- [TLS-CERTS] - http://tools.ietf.org/html/rfc6125 --- [XMPP-CORE] - http://tools.ietf.org/html/rfc6120 --- [SRV-ID] - http://tools.ietf.org/html/rfc4985 --- [IDNA] - http://tools.ietf.org/html/rfc5890 --- [LDAP] - http://tools.ietf.org/html/rfc4519 --- [PKIX] - http://tools.ietf.org/html/rfc5280 +-- [TLS-CERTS] - https://www.rfc-editor.org/rfc/rfc6125.html +-- [XMPP-CORE] - https://www.rfc-editor.org/rfc/rfc6120.html +-- [SRV-ID] - https://www.rfc-editor.org/rfc/rfc4985.html +-- [IDNA] - https://www.rfc-editor.org/rfc/rfc5890.html +-- [LDAP] - https://www.rfc-editor.org/rfc/rfc4519.html +-- [PKIX] - https://www.rfc-editor.org/rfc/rfc5280.html -local nameprep = require "util.encodings".stringprep.nameprep; -local idna_to_ascii = require "util.encodings".idna.to_ascii; -local idna_to_unicode = require "util.encodings".idna.to_unicode; -local base64 = require "util.encodings".base64; -local log = require "util.logger".init("x509"); -local mt = require "util.multitable"; +local nameprep = require "prosody.util.encodings".stringprep.nameprep; +local idna_to_ascii = require "prosody.util.encodings".idna.to_ascii; +local idna_to_unicode = require "prosody.util.encodings".idna.to_unicode; +local base64 = require "prosody.util.encodings".base64; +local log = require "prosody.util.logger".init("x509"); +local mt = require "prosody.util.multitable"; local s_format = string.format; local ipairs = ipairs; diff -r 54a936345aaa -r 5d9ec2e55d74 util/xml.lua --- a/util/xml.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/xml.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,5 +1,5 @@ -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local lxp = require "lxp"; local t_insert = table.insert; local t_remove = table.remove; diff -r 54a936345aaa -r 5d9ec2e55d74 util/xmppstream.lua --- a/util/xmppstream.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/xmppstream.lua Wed Mar 27 15:39:03 2024 +0000 @@ -7,7 +7,7 @@ -- local lxp = require "lxp"; -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local stanza_mt = st.stanza_mt; local error = error; diff -r 54a936345aaa -r 5d9ec2e55d74 util/xpcall.lua --- a/util/xpcall.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/xpcall.lua Wed Mar 27 15:39:03 2024 +0000 @@ -1,7 +1,7 @@ local xpcall = xpcall; if select(2, xpcall(function (x) return x end, function () end, "test")) ~= "test" then - xpcall = require"util.compat".xpcall; + xpcall = require"prosody.util.compat".xpcall; end return { diff -r 54a936345aaa -r 5d9ec2e55d74 util/xtemplate.lua --- a/util/xtemplate.lua Wed Mar 27 15:35:15 2024 +0000 +++ b/util/xtemplate.lua Wed Mar 27 15:39:03 2024 +0000 @@ -3,13 +3,21 @@ local s_sub = string.sub; local t_concat = table.concat; -local st = require("util.stanza"); +local st = require("prosody.util.stanza"); local function render(template, root, escape, filters) escape = escape or st.xml_escape; - return (s_gsub(template, "%b{}", function(block) + return (s_gsub(template, "(%s*)(%b{})(%s*)", function(pre_blank, block, post_blank) local inner = s_sub(block, 2, -2); + if inner:sub(1, 1) == "-" then + pre_blank = ""; + inner = inner:sub(2); + end + if inner:sub(-1, -1) == "-" then + post_blank = ""; + inner = inner:sub(1, -2); + end local path, pipe, pos = s_match(inner, "^([^|]+)(|?)()"); if not (type(path) == "string") then return end local value @@ -74,12 +82,12 @@ if type(value) == "string" then if not is_escaped then value = escape(value); end - return value + return pre_blank .. value .. post_blank elseif st.is_stanza(value) then value = value:get_text(); - if value then return escape(value) end + if value then return pre_blank .. escape(value) .. post_blank end end - return "" + return pre_blank .. post_blank end)) end