diff options
400 files changed, 16169 insertions, 5063 deletions
diff --git a/.editorconfig b/.editorconfig index 5bc9e002..39fc7da2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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_style = space indent_size = 3 indent_style = space -[**.xml] +[*.xml] +# xmllint --nsclean --encode UTF-8 --noent --format - indent_size = 2 indent_style = space diff --git a/.lua-format b/.lua-format new file mode 100644 index 00000000..9207ba55 --- /dev/null +++ b/.lua-format @@ -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 --git a/.luacheckrc b/.luacheckrc index 85035b86..90e18ce5 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -2,7 +2,7 @@ cache = true 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 @@ files["plugins/"] = { "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 @@ files["plugins/"] = { "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 @@ files["plugins/"] = { "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 @@ if os.getenv("PROSODY_STRICT_LINT") ~= "1" then "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 @@ if os.getenv("PROSODY_STRICT_LINT") ~= "1" then "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 --git a/.semgrep.yml b/.semgrep.yml index de1ef89e..22bfcfea 100644 --- a/.semgrep.yml +++ b/.semgrep.yml @@ -22,3 +22,9 @@ rules: 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] @@ -1,3 +1,87 @@ +TRUNK +===== + +## New + +### Administration + +- Add 'watch log' command to follow live debug logs at runtime (even if disabled) +- mod_announce: Add shell commands to send messages to all users, online users, or limited by roles + +### 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 + +- Component admins are no longer room owners by default. This can be reverted + to the old behaviour with `component_admins_as_room_owners = true`, but this + has known incompatibilities with some clients. Instead, use the shell or + ad-hoc commands to gain ownership of rooms when necessary. +- 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 +- Support for Type=notify and notify-reload systemd service type added +- Support for the roster *group* access_model in mod_pep +- Support for systemd socket activation in server_epoll +- mod_invites_adhoc gained a command for creating password resets + +## Removed + +- Lua 5.1 support +- XEP-0090 support removed from mod_time +- util.rfc6724 + 0.12.0 ====== diff --git a/GNUmakefile b/GNUmakefile index e9ec78c4..ec51c893 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -42,6 +42,10 @@ install-bin: prosody.install prosodyctl.install $(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-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so 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-meta: 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 @@ clean: $(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 @@ lint: @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 --git a/certs/openssl.cnf b/certs/openssl.cnf index ee17b1cf..8a05ecc3 100644 --- a/certs/openssl.cnf +++ b/certs/openssl.cnf @@ -46,7 +46,7 @@ subjectAltName = @subject_alternative_name [ 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 @@ -45,7 +45,7 @@ Configure $APP_NAME prior to building. 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 @@ do --lua-version|--with-lua-version) [ -n "$value" ] || die "Missing value in flag $key." LUA_VERSION="$value" - [ "$LUA_VERSION" = "5.1" ] || [ "$LUA_VERSION" = "5.2" ] || [ "$LUA_VERSION" = "5.3" ] || [ "$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 @@ if [ "$OSPRESET_SET" = "yes" ]; then 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 @@ if [ "$OSPRESET_SET" = "yes" ]; then 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 @@ then 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 @@ search_interpreter() { 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 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 --git a/core/certmanager.lua b/core/certmanager.lua index 6a46588c..9e0ace6a 100644 --- a/core/certmanager.lua +++ b/core/certmanager.lua @@ -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 next = next; 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 function index_certs(dir, files_by_name, depth_limit) 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 @@ local core_defaults = { 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,9 +210,21 @@ local core_defaults = { "!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" }; } +-- https://datatracker.ietf.org/doc/html/rfc7919#appendix-A.1 +local ffdhe2048 = [[ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- +]] + local mozilla_ssl_configs = { -- https://wiki.mozilla.org/Security/Server_Side_TLS -- Version 5.7 as of 2023-07-09 @@ -250,7 +237,7 @@ local mozilla_ssl_configs = { }; intermediate = { protocol = "tlsv1_2+"; - dhparam = nil; -- ffdhe2048.txt + dhparam = ffdhe2048; options = { cipher_server_preference = false }; ciphers = { "ECDHE-ECDSA-AES128-GCM-SHA256"; @@ -304,9 +291,9 @@ local mozilla_ssl_configs = { }; -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 +301,6 @@ else 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 +334,12 @@ local function create_context(host, mode, ...) 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 +378,16 @@ end 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 --git a/core/configmanager.lua b/core/configmanager.lua index 092b3946..bd12e169 100644 --- a/core/configmanager.lua +++ b/core/configmanager.lua @@ -11,14 +11,15 @@ local setmetatable, rawget, rawset, io, os, error, dofile, type, pairs, ipairs = 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 @@ function _M.getconfig() 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 @@ local function set(config_table, host, key, value) 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 @@ do 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 @@ do 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 --git a/core/features.lua b/core/features.lua index 35df5636..99edde51 100644 --- a/core/features.lua +++ b/core/features.lua @@ -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 --git a/core/hostmanager.lua b/core/hostmanager.lua index f33a3e1e..e27250de 100644 --- a/core/hostmanager.lua +++ b/core/hostmanager.lua @@ -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 --git a/core/loggingmanager.lua b/core/loggingmanager.lua index 5da77354..1717f059 100644 --- a/core/loggingmanager.lua +++ b/core/loggingmanager.lua @@ -6,21 +6,21 @@ -- 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"); -have_pposix = have_pposix and pposix._VERSION == "0.4.0"; +local have_pposix, pposix = pcall(require, "prosody.util.pposix"); +have_pposix = have_pposix and pposix._VERSION == "0.4.1"; local _ENV = nil; -- luacheck: std none diff --git a/core/moduleapi.lua b/core/moduleapi.lua index cbb2da9c..fa5086cf 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -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 @@ function api:wrap_global(event, handler) 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 @@ function api:depends(name) 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 @@ function api:shared(path) 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 @@ function api:get_option_string(name, default_value) 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 @@ function api:get_option_path(name, default, parent) 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 @@ function api:remove_item(key, value) 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 @@ function api:load_resource(path, mode) 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 @@ function api:measure(name, stat_type, conf) 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 ~= "*" label_keys = label_keys or {}; if is_scoped then @@ -579,7 +674,7 @@ local status_priorities = { error = 3, warn = 2, info = 1, core = 0 }; 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; @@ -602,4 +697,107 @@ function api:get_status() 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 --git a/core/modulemanager.lua b/core/modulemanager.lua index 0ddf175b..873e08e5 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -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 @@ local function do_load_module(host, module_name, state) 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 --git a/core/portmanager.lua b/core/portmanager.lua index 38c74b66..904c979c 100644 --- a/core/portmanager.lua +++ b/core/portmanager.lua @@ -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 @@ local function error_to_friendly_message(service_name, port, err) --luacheck: ig 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 @@ function get_service_at(interface, port) 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 @@ local function add_sni_host(host, service) 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 @@ prosody.events.add_handler("host-deactivated", function (host) 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 @@ return { 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 --git a/core/rostermanager.lua b/core/rostermanager.lua index efb80abb..3ada40c9 100644 --- a/core/rostermanager.lua +++ b/core/rostermanager.lua @@ -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 type = type; 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 --git a/core/s2smanager.lua b/core/s2smanager.lua index b9190993..d2c58a54 100644 --- a/core/s2smanager.lua +++ b/core/s2smanager.lua @@ -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 --git a/core/sessionmanager.lua b/core/sessionmanager.lua index f8279bb4..750007fb 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -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 initialize_filters = require "util.filters".initialize; +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 "prosody.util.filters".initialize; local gettime = require "socket".gettime; local _ENV = nil; @@ -92,6 +92,51 @@ local function retire_session(session) 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 @@ local function destroy_session(session, err) 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 @@ end 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 --git a/core/stanza_router.lua b/core/stanza_router.lua index b54ea1ab..197c5f64 100644 --- a/core/stanza_router.lua +++ b/core/stanza_router.lua @@ -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 @@ function core_process_stanza(origin, stanza) 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 @@ function core_process_stanza(origin, stanza) 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 --git a/core/statsmanager.lua b/core/statsmanager.lua index 686fc895..f5b7e356 100644 --- a/core/statsmanager.lua +++ b/core/statsmanager.lua @@ -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 @@ if stats_interval_config == "manual" then end local builtin_providers = { - internal = "util.statistics"; - statsd = "util.statsd"; + internal = "prosody.util.statistics"; + statsd = "prosody.util.statsd"; }; diff --git a/core/storagemanager.lua b/core/storagemanager.lua index 856acad3..238c7612 100644 --- a/core/storagemanager.lua +++ b/core/storagemanager.lua @@ -3,12 +3,12 @@ local type, pairs = type, pairs; 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 @@ local function load_driver(host, driver_name) 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 map_shim_mt = { }; } +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 @@ local function create_map_shim(host, store) }, 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 --git a/core/usermanager.lua b/core/usermanager.lua index 45f104fa..793e7af6 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -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 setmetatable = setmetatable; local default_provider = "internal_hashed"; +local debug = debug; + local _ENV = nil; -- luacheck: std none @@ -36,26 +34,26 @@ local function new_null_provider() }); 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; -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_role = function (user) end; + set_user_role = function (user, role_name) end; + + 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 function initialize_host(host) 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 @@ local function set_password(username, password, host, resource) 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 @@ local function delete_user(username, host) 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 @@ local function get_provider(host) 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 actor_user, actor_host = jid_split(jid); - local roles; +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 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 set_roles(jid, host, roles) +local function user_can_assume_role(user, host, role_name) 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.user_can_assume_role(user, role_name); +end - local actor_user, actor_host = jid_split(jid); +local function add_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 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 + return hosts[host].authz.get_jids_with_role(role); +end - host = host or "*"; +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 authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - return authz_provider.get_jids_with_role(role); +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 @@ return { 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; - is_admin = is_admin; + 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; }; diff --git a/doc/doap.xml b/doc/doap.xml index fa3893f8..cf150c36 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -56,18 +56,23 @@ <implements rdf:resource="https://www.rfc-editor.org/info/rfc6455"/> <implements rdf:resource="https://www.rfc-editor.org/info/rfc6901"/> <implements rdf:resource="https://www.rfc-editor.org/info/rfc7233"/> + <implements rdf:resource="https://www.rfc-editor.org/info/rfc7239"/> <implements rdf:resource="https://www.rfc-editor.org/info/rfc7301"/> <implements rdf:resource="https://www.rfc-editor.org/info/rfc7395"/> <implements rdf:resource="https://www.rfc-editor.org/info/rfc7590"/> + <implements rdf:resource="https://www.rfc-editor.org/info/rfc7628"/> <implements rdf:resource="https://www.rfc-editor.org/info/rfc7673"/> + <implements rdf:resource="https://www.rfc-editor.org/info/rfc8305"/> + <implements rdf:resource="https://www.rfc-editor.org/info/rfc9266"/> <implements rdf:resource="https://datatracker.ietf.org/doc/draft-cridland-xmpp-session/"> <!-- since=0.6.0 note=Added in hg:0bbbc9042361 --> </implements> + <implements rdf:resource="https://datatracker.ietf.org/doc/draft-ietf-dance-client-auth"/> <implements rdf:resource="http://www.unicode.org/reports/tr39/"/> <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/> - <xmpp:version>2.12.1</xmpp:version> + <xmpp:version>2.13.1</xmpp:version> <xmpp:since>0.4.0</xmpp:since> <xmpp:status>partial</xmpp:status> <xmpp:note>no support for multiple items (reported tag)</xmpp:note> @@ -102,7 +107,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/> - <xmpp:version>2.5rc3</xmpp:version> + <xmpp:version>2.5.0</xmpp:version> <xmpp:since>0.10.0</xmpp:since> <xmpp:status>complete</xmpp:status> </xmpp:SupportedXep> @@ -119,7 +124,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/> - <xmpp:version>1.34.1</xmpp:version> + <xmpp:version>1.34.5</xmpp:version> <xmpp:since>0.3.0</xmpp:since> <xmpp:status>partial</xmpp:status> </xmpp:SupportedXep> @@ -172,7 +177,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/> - <xmpp:version>1.22.0</xmpp:version> + <xmpp:version>1.24.1</xmpp:version> <xmpp:since>0.9.0</xmpp:since> <xmpp:status>partial</xmpp:status> <xmpp:note>mod_pubsub</xmpp:note> @@ -240,7 +245,8 @@ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0090.html"/> <xmpp:version>1.2</xmpp:version> <xmpp:since>0.1.0</xmpp:since> - <xmpp:status>complete</xmpp:status> + <xmpp:until>trunk</xmpp:until> + <xmpp:status>removed</xmpp:status> <xmpp:note>mod_time</xmpp:note> </xmpp:SupportedXep> </implements> @@ -268,12 +274,13 @@ <xmpp:version>1.0</xmpp:version> <xmpp:since>0.9.0</xmpp:since> <xmpp:status>complete</xmpp:status> + <xmpp:note>util.jid.(un)escape, missing rejection of \20 at start or end per xep version 1.1. Missing PRECIS for version 1.1.1.</xmpp:note> </xmpp:SupportedXep> </implements> <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0107.html"/> - <xmpp:version>1.2.1</xmpp:version> + <xmpp:version>1.2.2</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:note>via XEP-0163</xmpp:note> </xmpp:SupportedXep> @@ -297,7 +304,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/> - <xmpp:version>1.5.2</xmpp:version> + <xmpp:version>1.6.0</xmpp:version> <xmpp:since>0.8.0</xmpp:since> <xmpp:status>complete</xmpp:status> </xmpp:SupportedXep> @@ -346,7 +353,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0133.html"/> - <xmpp:version>1.2</xmpp:version> + <xmpp:version>1.3.1</xmpp:version> <xmpp:since>0.7.0</xmpp:since> <xmpp:status>partial</xmpp:status> <xmpp:note>mod_admin_adhoc, missing some commands</xmpp:note> @@ -355,7 +362,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0138.html"/> - <xmpp:version>2.0</xmpp:version> + <xmpp:version>2.1</xmpp:version> <xmpp:since>0.6.0</xmpp:since> <xmpp:until>0.10.0</xmpp:until> <xmpp:status>removed</xmpp:status> @@ -365,7 +372,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0153.html"/> - <xmpp:version>1.1</xmpp:version> + <xmpp:version>1.1.1</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>via XEP-0398</xmpp:note> @@ -390,7 +397,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/> - <xmpp:version>1.2.1</xmpp:version> + <xmpp:version>1.2.2</xmpp:version> <xmpp:since>0.5.0</xmpp:since> <xmpp:status>complete</xmpp:status> </xmpp:SupportedXep> @@ -501,7 +508,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/> - <xmpp:version>1.6</xmpp:version> + <xmpp:version>1.6.2</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.12.0</xmpp:since> <xmpp:note>mod_smacks</xmpp:note> @@ -561,7 +568,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0215.html"/> - <xmpp:version>0.7.1</xmpp:version> + <xmpp:version>1.0.0</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.12.0</xmpp:since> <xmpp:note>mod_external_services</xmpp:note> @@ -587,7 +594,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0223.html"/> - <xmpp:version>1.1</xmpp:version> + <xmpp:version>1.1.1</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_pep</xmpp:note> @@ -623,7 +630,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/> - <xmpp:version>1.0.0</xmpp:version> + <xmpp:version>1.0.1</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.10.0</xmpp:since> </xmpp:SupportedXep> @@ -648,7 +655,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0292.html"/> - <xmpp:version>0.11</xmpp:version> + <xmpp:version>0.12.0</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.11.0</xmpp:since> <xmpp:note>mod_vcard4, mod_vcard_legacy</xmpp:note> @@ -657,7 +664,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html"/> - <xmpp:version>1.0.0</xmpp:version> + <xmpp:version>1.0</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>Used by XEP-0280, XEP-0313</xmpp:note> @@ -683,7 +690,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/> - <xmpp:version>1.0.0</xmpp:version> + <xmpp:version>1.1.2</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.10.0</xmpp:since> <xmpp:note>mod_mam, mod_muc_mam</xmpp:note> @@ -692,8 +699,8 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0317.html"/> - <xmpp:version>0.1</xmpp:version> - <xmpp:status>planned</xmpp:status> + <xmpp:version>0.2.0</xmpp:version> + <xmpp:status>complete</xmpp:status> <xmpp:since>0.12.0</xmpp:since> <xmpp:note>muc/hats</xmpp:note> </xmpp:SupportedXep> @@ -719,7 +726,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html"/> - <xmpp:version>0.4.0</xmpp:version> + <xmpp:version>0.6.0</xmpp:version> <xmpp:since>0.11.6</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>triggers buffer flush in mod_csi_simple since 0.11.6; recognised by mod_carbons and mod_mam since 0.12</xmpp:note> @@ -728,7 +735,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html"/> - <xmpp:version>0.6.1</xmpp:version> + <xmpp:version>0.7.0</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.10.0</xmpp:since> <xmpp:note>Used in context of XEP-0313 by mod_mam and mod_muc_mam</xmpp:note> @@ -737,7 +744,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0363.html"/> - <xmpp:version>1.0.0</xmpp:version> + <xmpp:version>1.1.0</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:since>0.12.0</xmpp:since> <xmpp:note>mod_http_file_share</xmpp:note> @@ -763,7 +770,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/> - <xmpp:version>0.3.0</xmpp:version> + <xmpp:version>0.4.0</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>Used in context of XEP-0352</xmpp:note> @@ -772,7 +779,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0384.html"/> - <xmpp:version>0.8.1</xmpp:version> + <xmpp:version>0.8.3</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:note>via XEP-0163, XEP-0222</xmpp:note> </xmpp:SupportedXep> @@ -780,7 +787,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0398.html"/> - <xmpp:version>0.2.1</xmpp:version> + <xmpp:version>1.0.0</xmpp:version> <xmpp:since>0.11.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_vcard_legacy</xmpp:note> @@ -789,7 +796,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0401.html"/> - <xmpp:version>0.3.0</xmpp:version> + <xmpp:version>0.5.0</xmpp:version> <xmpp:since>0.12.0</xmpp:since> <xmpp:status>partial</xmpp:status> </xmpp:SupportedXep> @@ -797,7 +804,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0402.html"/> - <xmpp:version>1.1.3</xmpp:version> + <xmpp:version>1.2.0</xmpp:version> <xmpp:since>0.12.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_bookmarks</xmpp:note> @@ -824,7 +831,7 @@ <implements> <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0421.html"/> - <xmpp:version>0.1.0</xmpp:version> + <xmpp:version>0.2.0</xmpp:version> <xmpp:since>0.12.0</xmpp:since> <xmpp:status>complete</xmpp:status> <xmpp:note>mod_muc</xmpp:note> @@ -839,11 +846,34 @@ </implements> <implements> <xmpp:SupportedXep> + <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0440.html"/> + <xmpp:version>0.4.2</xmpp:version> + <xmpp:since>trunk</xmpp:since> + <xmpp:status>complete</xmpp:status> + </xmpp:SupportedXep> + </implements> + <implements> + <xmpp:SupportedXep> <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0441.html"/> <xmpp:version>0.2.0</xmpp:version> <xmpp:status>complete</xmpp:status> <xmpp:note>Broken out of XEP-0313</xmpp:note> </xmpp:SupportedXep> </implements> + <implements> + <xmpp:SupportedXep> + <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0445.html"/> + <xmpp:version>0.2.0</xmpp:version> + <xmpp:since>0.12.0</xmpp:since> + <xmpp:status>complete</xmpp:status> + </xmpp:SupportedXep> + </implements> + <implements> + <xmpp:SupportedXep> + <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0478.html"/> + <xmpp:version>0.2.0</xmpp:version> + <xmpp:since>trunk</xmpp:since> + </xmpp:SupportedXep> + </implements> </Project> </rdf:RDF> diff --git a/doc/hgrc-email.ini b/doc/hgrc-email.ini new file mode 100644 index 00000000..91aedc33 --- /dev/null +++ b/doc/hgrc-email.ini @@ -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 --git a/doc/hgrc.ini b/doc/hgrc.ini new file mode 100644 index 00000000..43a738a6 --- /dev/null +++ b/doc/hgrc.ini @@ -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 --git a/doc/storage.tld b/doc/storage.tld deleted file mode 100644 index 0fc8ff54..00000000 --- a/doc/storage.tld +++ /dev/null @@ -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 --git a/loader.lua b/loader.lua new file mode 100644 index 00000000..9c2176a7 --- /dev/null +++ b/loader.lua @@ -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; +}) @@ -44,6 +44,10 @@ install-bin: prosody.install prosodyctl.install $(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-util: util/encodings.so util/encodings.so util/pposix.so util/signal.so 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-meta: 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 --git a/net/adns.lua b/net/adns.lua index ae168b9c..2ecd7f53 100644 --- a/net/adns.lua +++ b/net/adns.lua @@ -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 --git a/net/connect.lua b/net/connect.lua index 4b602be4..0e39cf36 100644 --- a/net/connect.lua +++ b/net/connect.lua @@ -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 pending_connection_listeners = {}; 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 @@ local function attempt_connection(p) 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 @@ function pending_connection_listeners.onconnect(conn) 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 @@ function pending_connection_listeners.ondisconnect(conn, reason) 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 @@ local function connect(target_resolver, listeners, options, data) listeners = assert(listeners); options = options or {}; data = data; + conns = {}; }, pending_connection_mt); p:log("debug", "Starting connection process"); diff --git a/net/cqueues.lua b/net/cqueues.lua index 65d2a019..72d8759f 100644 --- a/net/cqueues.lua +++ b/net/cqueues.lua @@ -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 --git a/net/dns.lua b/net/dns.lua index a9846e86..ba432c14 100644 --- a/net/dns.lua +++ b/net/dns.lua @@ -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 --git a/net/http.lua b/net/http.lua index e81975fd..35a92d57 100644 --- a/net/http.lua +++ b/net/http.lua @@ -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 pairs = pairs; 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 @@ local function log_if_failed(req, ret, ...) 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 @@ local function request_reader(request, data, err) 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 @@ function listener.ondisconnect(conn, err) 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 @@ local function request(self, u, ex, callback) ["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 @@ -299,6 +322,23 @@ local function request(self, u, ex, callback) 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); @@ -335,6 +375,10 @@ local function new(options) 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 --git a/net/http/codes.lua b/net/http/codes.lua index 4327f151..b2949286 100644 --- a/net/http/codes.lua +++ b/net/http/codes.lua @@ -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 @@ local response_codes = { [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 --git a/net/http/errors.lua b/net/http/errors.lua index 1691e426..ca5f0ccc 100644 --- a/net/http/errors.lua +++ b/net/http/errors.lua @@ -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 --git a/net/http/files.lua b/net/http/files.lua index 01c46a2c..8ef054e2 100644 --- a/net/http/files.lua +++ b/net/http/files.lua @@ -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 @@ if package.config:sub(1,1) == "\\" then 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 --git a/net/http/parser.lua b/net/http/parser.lua index 12d40883..b7e8b46c 100644 --- a/net/http/parser.lua +++ b/net/http/parser.lua @@ -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 --git a/net/http/server.lua b/net/http/server.lua index 43e6bc9f..1ef42b2e 100644 --- a/net/http/server.lua +++ b/net/http/server.lua @@ -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 @@ end 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 @@ function listener.onconnect(conn) 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); @@ -197,7 +206,7 @@ local function handle_result(request, response, result) end elseif result_type == "string" then body = result; - elseif errors.is_err(result) then + elseif errors.is_error(result) then response.status_code = result.code or 500; body = events.fire_event("http-error", { request = request, response = response, code = result.code or 500, error = result }); elseif promise.is_promise(result) then @@ -232,6 +241,8 @@ function handle_request(conn, request, finish_cb) 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 @@ function handle_request(conn, request, finish_cb) 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 @@ function handle_request(conn, request, finish_cb) 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 @@ function handle_request(conn, request, finish_cb) 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 @@ end 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 @@ function _M.send_file(response, f) 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 @@ function _M.finish_response(response) 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 --git a/net/httpserver.lua b/net/httpserver.lua index 6b14313b..0dfd862e 100644 --- a/net/httpserver.lua +++ b/net/httpserver.lua @@ -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 --git a/net/resolvers/basic.lua b/net/resolvers/basic.lua index 305bce76..d9d33310 100644 --- a/net/resolvers/basic.lua +++ b/net/resolvers/basic.lua @@ -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 @@ function methods:next(cb) 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 @@ function methods:next(cb) 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 @@ local function new(hostname, port, conn_type, extra) hostname = ascii_host; port = port; conn_type = conn_type; - extra = extra; + extra = extra or {}; targets = targets; }, resolver_mt); end diff --git a/net/resolvers/manual.lua b/net/resolvers/manual.lua index dbc40256..c766a11f 100644 --- a/net/resolvers/manual.lua +++ b/net/resolvers/manual.lua @@ -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 --git a/net/resolvers/service.lua b/net/resolvers/service.lua index 3810cac8..d4318bb5 100644 --- a/net/resolvers/service.lua +++ b/net/resolvers/service.lua @@ -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 @@ function methods:next(cb) end end); return; + elseif self.in_progress then + cb(nil); + return; end if not self.hostname then @@ -39,9 +97,9 @@ function methods:next(cb) return; end - local targets = {}; + self.in_progress = true; + local function ready() - self.targets = targets; self:next(cb); end @@ -53,17 +111,23 @@ function methods:next(cb) 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 @@ function methods:next(cb) 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 --git a/net/server.lua b/net/server.lua index 0696fd52..942aceb2 100644 --- a/net/server.lua +++ b/net/server.lua @@ -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 = "select"; +local server_type = default_backend; -local default_backend = "epoll"; +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; -local server_type = require "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 @@ end 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 @@ if server_type == "event" then 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 @@ elseif server_type == "select" then 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 @@ else 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 @@ else 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 @@ if prosody and set_config then 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 --git a/net/server_epoll.lua b/net/server_epoll.lua index fa275d71..b4477375 100644 --- a/net/server_epoll.lua +++ b/net/server_epoll.lua @@ -6,8 +6,6 @@ -- -local t_insert = table.insert; -local t_concat = table.concat; local setmetatable = setmetatable; local pcall = pcall; local type = type; @@ -15,24 +13,59 @@ local next = next; 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 dbuffer = require "prosody.util.dbuffer"; +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; +-- systemd socket activation +local SD_LISTEN_FDS_START = 3; +local SD_LISTEN_FDS = tonumber(os.getenv("LISTEN_FDS")) or 0; + +local inherited_sockets = setmetatable({}, { + __index = function(t, k) + local serv_mt = debug.getregistry()["tcp{server}"]; + for i = 1, SD_LISTEN_FDS do + local serv = socket.tcp(); + if serv:getfd() ~= _SOCKETINVALID then + -- If LuaSocket allocated a FD for then we can't really close it and it would leak. + log("error", "LuaSocket not compatible with socket activation. Upgrade LuaSocket or disable socket activation."); + setmetatable(t, nil); + break + end + serv:setfd(SD_LISTEN_FDS_START + i - 1); + debug.setmetatable(serv, serv_mt); + serv:settimeout(0); + local ip, port = serv:getsockname(); + t[ip .. ":" .. port] = serv; + if ip == "0.0.0.0" then + -- LuaSocket treats '*' as an alias for '0.0.0.0' + t["*:" .. port] = serv; + end + end + + -- Disable lazy-loading mechanism once performed + setmetatable(t, nil); + return t[k]; + end; +}); + local poll = assert(poller.new()); local _ENV = nil; @@ -60,6 +93,15 @@ local default_config = { __index = { -- Size of chunks to read from sockets read_size = 8192; + -- Maximum size of send buffer, after which additional data is rejected + max_send_buffer_size = 32*1024*1024; + + -- How many chunks (immutable strings) to keep in the send buffer + send_buffer_chunks = nil; + + -- Maximum amount of data to send at once (to the TCP buffers), default based on /proc/sys/net/ipv4/tcp_wmem + max_send_chunk = 4*1024*1024; + -- Timeout used during between steps in TLS handshakes ssl_handshake_timeout = 60; @@ -91,6 +133,12 @@ local default_config = { __index = { --- 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 +441,9 @@ function interface:set(r, w) 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); @@ -457,7 +508,8 @@ function interface:onreadable() end if err == "closed" and self._connected then self:debug("Connection closed by remote"); - self:close(err); + self:on("disconnect", err); + self:destroy(); return; elseif err ~= "timeout" then self:debug("Read error, closing (%s)", err); @@ -490,26 +542,21 @@ function interface:onwritable() self:onconnect(); if not self.conn then return nil, "no-conn"; end -- could have been closed in onconnect self:on("predrain"); - local buffer = self.writebuffer; - local data = buffer or ""; - if type(buffer) == "table" then - if buffer[3] then - data = t_concat(data); - elseif buffer[2] then - data = buffer[1] .. buffer[2]; - else - data = buffer[1] or ""; - end - end + local buffer = self.writebuffer or ""; + -- Naming things ... s/data/slice/ ? + local data = buffer:sub(1, cfg.max_send_chunk); local ok, err, partial = self.conn:send(data); self._writable = ok; - if ok then + if ok and #data < #buffer then + -- Sent the whole 'data' but there's more in the buffer + ok, err, partial = nil, "timeout", ok; + end + self:debug("Sent %d out of %d buffered bytes", ok and #data or partial or 0, #buffer); + if ok then -- all the data we had was sent successfully self:set(nil, false); if cfg.keep_buffers and type(buffer) == "table" then - for i = #buffer, 1, -1 do - buffer[i] = nil; - end - else + buffer:discard(ok); + else -- string or don't keep buffers self.writebuffer = nil; end self._writing = nil; @@ -517,14 +564,10 @@ function interface:onwritable() self:ondrain(); -- Be aware of writes in ondrain return ok; elseif partial then - self:debug("Sent %d out of %d buffered bytes", partial, #data); - if cfg.keep_buffers and type(buffer) == "table" then - buffer[1] = data:sub(partial+1); - for i = #buffer, 2, -1 do - buffer[i] = nil; - end + if type(buffer) == "table" then + buffer:discard(partial); else - self.writebuffer = data:sub(partial+1); + self.writebuffer = data:sub(partial + 1); end self:set(nil, true); self:setwritetimeout(); @@ -552,13 +595,51 @@ end -- Add data to write buffer and set flag for wanting to write function interface:write(data) local buffer = self.writebuffer; - if type(buffer) == "table" then - t_insert(buffer, data); - elseif type(buffer) == "string" then - self:noise("Allocating buffer!") - self.writebuffer = { buffer, data }; - elseif buffer == nil then + -- (nil) -> save string + -- (string) -> convert to buffer (3 tables!) + -- (buffer) -> write to buffer + if not buffer then self.writebuffer = data; + elseif type(buffer) == "string" then + local prev_buffer = buffer; + buffer = dbuffer.new(cfg.max_send_buffer_size, cfg.send_buffer_chunks); + self.writebuffer = buffer; + if prev_buffer then + -- TODO refactor, there's 3 copies of these lines + if not buffer:write(prev_buffer) then + if self._write_lock then + return false; + end + -- Try to flush buffer to make room + self:onwritable(); + if not buffer:write(prev_buffer) then + self:on("disconnect", "no space left in buffer"); + self:destroy(); + return false; + end + end + end + if not buffer:write(data) then + if self._write_lock then + return false; + end + self:onwritable(); + if not buffer:write(data) then + self:on("disconnect", "no space left in buffer"); + self:destroy(); + return false; + end + end + elseif not buffer:write(data) then + if self._write_lock then + return false; + end + self:onwritable(); + if not buffer:write(data) then + self:on("disconnect", "no space left in buffer"); + self:destroy(); + return false; + end end if not self._write_lock and not self._writing then if self._writable and cfg.opportunistic_writes and not self._opportunistic_write then @@ -576,7 +657,7 @@ interface.send = interface.write; -- Close, possibly after writing is done function interface:close() - if self._connected and self.writebuffer and (self.writebuffer[1] or type(self.writebuffer) == "string") then + if self.writebuffer and #self.writebuffer ~= 0 then self._connected = false; self:set(false, true); -- Flush final buffer contents self:setreadtimeout(false); @@ -614,10 +695,51 @@ function interface:set_sslctx(sslctx) 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; - if self.writebuffer and (self.writebuffer[1] or type(self.writebuffer) == "string") then + if self.writebuffer and #self.writebuffer ~= 0 then self:debug("Start TLS after write"); self.ondrain = interface.starttls; self:set(nil, true); -- make sure wantwrite is set @@ -641,11 +763,7 @@ function interface:inittls(tls_ctx, now) 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 +774,8 @@ function interface:inittls(tls_ctx, now) 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 +859,6 @@ local function wrapsocket(client, server, read_size, listeners, tls_ctx, extra) end end - conn:updatenames(); return conn; end @@ -767,6 +884,7 @@ function interface:onacceptable() 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; @@ -855,7 +973,7 @@ function interface:resume_writes() end self:noise("Resume writes"); self._write_lock = nil; - if self.writebuffer and (self.writebuffer[1] or type(self.writebuffer) == "string") then + if self.writebuffer and #self.writebuffer ~= 0 then self:setwritetimeout(); self:set(nil, true); end @@ -885,11 +1003,25 @@ local function wrapserver(conn, addr, port, listeners, config) 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 local function listen(addr, port, listeners, config) + local inherited = inherited_sockets[addr .. ":" .. port]; + if inherited then + local conn = wrapserver(inherited, addr, port, listeners, config); + -- sockets created by systemd must not be :close() since we may not have + -- privileges to create them + conn.destroy = interface.del; + return conn; + end local conn, err = socket.bind(addr, port, cfg.tcp_backlog); if not conn then return conn, err; end conn:settimeout(0); @@ -908,6 +1040,7 @@ end -- 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 +1074,13 @@ local function addclient(addr, port, listeners, read_size, tls_ctx, typ, extra) 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 +1169,38 @@ local function setquitting(quit) 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 +1213,35 @@ local function loop(once) 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 +1267,11 @@ return { 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 --git a/net/server_event.lua b/net/server_event.lua index e0235121..44222aa3 100644 --- a/net/server_event.lua +++ b/net/server_event.lua @@ -47,15 +47,17 @@ local s_sub = string.sub 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 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed _ = 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 @@ function interface_mt:_start_ssl(call_onconnect) -- old socket will be destroyed 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 @@ function interface_mt:pause() 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 @@ -939,6 +969,10 @@ return { 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 --git a/net/server_select.lua b/net/server_select.lua index eea850ce..cccf7a4e 100644 --- a/net/server_select.lua +++ b/net/server_select.lua @@ -11,7 +11,7 @@ local use = function( what ) 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 @@ local coroutine_yield = coroutine.yield --// 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ wrapconnection = function( server, listeners, socket, ip, serverport, clientport 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 @@ return { removeserver = removeserver, get_backend = get_backend, changesettings = changesettings, + + tls_builder = function(basedir) + return sslconfig._new(tls_impl.new_context, basedir) + end, } diff --git a/net/stun.lua b/net/stun.lua index 2c35786f..586f96e2 100644 --- a/net/stun.lua +++ b/net/stun.lua @@ -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 --git a/net/tls_luasec.lua b/net/tls_luasec.lua new file mode 100644 index 00000000..4e4e92ed --- /dev/null +++ b/net/tls_luasec.lua @@ -0,0 +1,117 @@ +-- 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" and cfg.dhparam:sub(1, 10) == "-----BEGIN" then + local dhparam = cfg.dhparam; + cfg.dhparam = function() return dhparam; end + elseif 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 --git a/net/unbound.lua b/net/unbound.lua index ee742b7c..176a6156 100644 --- a/net/unbound.lua +++ b/net/unbound.lua @@ -13,15 +13,15 @@ local s_lower = string.lower; 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 @@ end 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")); @@ -80,8 +80,12 @@ local answer_mt = { h = h .. s_format(", Bogus: %s", self.bogus); end local t = { h }; + local qname = self.canonname or self.qname; + if self.canonname then + table.insert(t, self.qname .. "\t" .. classes[self.qclass] .. "\tCNAME\t" .. self.canonname); + end for i = 1, #self do - t[i+1]=self.qname.."\t"..classes[self.qclass].."\t"..types[self.qtype].."\t"..tostring(self[i]); + table.insert(t, qname .. "\t" .. classes[self.qclass] .. "\t" .. types[self.qtype] .. "\t" .. tostring(self[i])); end local _string = t_concat(t, "\n"); self._string = _string; diff --git a/net/websocket.lua b/net/websocket.lua index 193cd556..708a7be7 100644 --- a/net/websocket.lua +++ b/net/websocket.lua @@ -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 --git a/net/websocket/frames.lua b/net/websocket/frames.lua index 6a088902..c22a12de 100644 --- a/net/websocket/frames.lua +++ b/net/websocket/frames.lua @@ -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 @@ local function parse_frame_header(frame) 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 --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua index 4cf6911d..0ce45e19 100644 --- a/plugins/adhoc/adhoc.lib.lua +++ b/plugins/adhoc/adhoc.lib.lua @@ -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,10 +23,16 @@ end function _M.new(name, node, handler, permission) if not permission then error "adhoc.new() expects a permission argument, none given" - end - if permission == "user" then + elseif permission == "user" then error "the permission mode 'user' has been renamed 'any', please update your code" end + 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 @@ function _M.handle_cmd(command, origin, stanza) 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 --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 09a72075..8abfff99 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -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 @@ module:hook("host-disco-info-node", function (event) 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 @@ module:hook("iq-set/host/"..xmlns_cmd..":command", function (event) 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 --git a/plugins/mod_admin_adhoc.lua b/plugins/mod_admin_adhoc.lua index 5a565fb3..ca84f975 100644 --- a/plugins/mod_admin_adhoc.lua +++ b/plugins/mod_admin_adhoc.lua @@ -14,23 +14,25 @@ local t_sort = table.sort; 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 @@ local delete_user_command_handler = adhoc_simple(delete_user_layout, function(fi "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 add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#ad 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", add_user_desc); 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 --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index f2da286b..c32048c3 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -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, jid_compare = import("util.jid", "bare", "prepped_split", "join", "compare"); -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 commands = module:shared("commands") 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 @@ function runner_callbacks:error(err) 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 @@ function console:new_session(admin_session) 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 @@ local function handle_line(event) 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 @@ local function handle_line(event) 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 @@ module:hook("admin/repl-input", function (event) 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 @@ end -- 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 @@ function def_env.output:configure(opts) 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 @@ function def_env.server:insane_reload() 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 @@ function def_env.server:uptime() 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 @@ local function human(kb) 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 @@ function def_env.server:memory() 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 @@ local function get_hosts_with_module(hosts, module) 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 @@ function def_env.module:info(name, hosts) 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 @@ function def_env.module:info(name, hosts) ["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,38 @@ function def_env.module:info(name, hosts) 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); + local already_loaded = set.new(); -- 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,19 +651,34 @@ function def_env.module:load(name, hosts, config) 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 + else + already_loaded:add(host); end end - return ok, (ok and "Module loaded onto "..count.." host"..(count ~= 1 and "s" or "")) or ("Last error: "..tostring(err)); + if not ok then + return ok, "Last error: "..tostring(err); + end + if already_loaded == hosts then + return ok, "Module already loaded"; + end + return ok, "Module loaded onto "..count.." host"..(count ~= 1 and "s" or ""); 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 +687,10 @@ function def_env.module:unload(name, hosts) 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 +703,7 @@ local function _sort_hosts(a, b) 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 +727,7 @@ function def_env.module:reload(name, hosts) 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 +754,10 @@ function def_env.module:list(hosts) 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 +765,30 @@ function def_env.config:load(filename, format) 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 +825,7 @@ local function show_c2s(callback) 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 +843,7 @@ available_columns = { 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 +851,7 @@ available_columns = { 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 +859,7 @@ available_columns = { remote = { title = "Remote"; description = "Remote hostname"; - width = 22; + width = "1p"; mapper = function(_, session) return select(2, get_s2s_hosts(session)); end; @@ -743,7 +867,7 @@ available_columns = { 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) @@ -752,10 +876,22 @@ available_columns = { end end; }; + created = { + title = "Connection Created"; + description = "Time when connection was created"; + width = #"YYYY MM DD HH:MM:SS"; + align = "right"; + key = "conn"; + mapper = function(conn) + if conn then + return os.date("%F %T", math.floor(conn.created)); + end + end; + }; 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 +899,23 @@ available_columns = { 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 +929,20 @@ available_columns = { 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 +953,22 @@ available_columns = { 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,27 +976,36 @@ available_columns = { 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 type(session.cert_chain_errors) == "table" 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 + -- TODO cert_chain_errors can be a string, handle that return "Unknown"; end; }; 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 +1015,7 @@ available_columns = { 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 +1026,8 @@ available_columns = { 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 +1061,7 @@ available_columns = { 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 +1075,16 @@ available_columns = { 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 +1092,7 @@ local function get_colspec(colspec, default) 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 +1103,15 @@ local function get_colspec(colspec, default) 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_compare(jid, match_jid); + return (not match_jid) or match_jid == "*" or jid_compare(jid, match_jid); end local group_by_host = true; @@ -973,6 +1144,7 @@ function def_env.c2s:show(match_jid, colspec) 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 +1158,7 @@ local function build_reason(text, condition) 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 +1170,7 @@ function def_env.c2s:close(match_jid, text, condition) 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 +1182,8 @@ function def_env.c2s:closeall(text, condition) 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 +1191,31 @@ local function _sort_s2s(a, b) 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 +1249,7 @@ function def_env.s2s:show(match_jid, colspec) 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 +1283,7 @@ function def_env.s2s:showcert(domain) 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 +1372,7 @@ function def_env.s2s:showcert(domain) .." 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 +1387,22 @@ function def_env.s2s:close(from, to, text, condition) 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 +1411,42 @@ function def_env.s2s:closeall(host, text, condition) 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 +1465,7 @@ function def_env.port:list() 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 +1488,7 @@ function def_env.port:close(close_port, close_interface) 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 +1507,21 @@ local function check_muc(jid) 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 +1533,16 @@ function def_env.muc:create(room_jid, config) 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 +1557,160 @@ function def_env.muc:list(host) 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 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 -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; + 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 +1726,7 @@ function def_env.user:delete(jid) 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 +1742,79 @@ function def_env.user:password(jid, password) 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 "<none>"); + + 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 userhost == host then + return um.set_user_role(username, userhost, new_role); + else + return um.set_jid_role(jid, host, new_role); + end +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"; + elseif userhost ~= host then + return nil, "Can't add roles outside users own host" + 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"; + elseif userhost ~= host then + return nil, "Can't remove roles outside users own host" end - if host == "*" then host = nil; end - return um.set_roles(jid, host, coerce_roles(new_roles)); + 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 +1834,10 @@ function def_env.user:list(host, pat) 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 +1884,12 @@ function def_env.xmpp:ping(localhost, remotehost, timeout) 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 +1900,54 @@ local function get_resolver(session) 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 +1958,14 @@ function def_env.http:list(hosts) 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 +1980,83 @@ function def_env.http:list(hosts) 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<!-- sent to %s -->"):format(session.id)); + elseif event_type == "received" then + self.session.print(("\n<!-- received from %s -->"):format(session.id)); + else + self.session.print(("\n<!-- %s (%s) -->"):format(event_type, session.id)); + end + self.session.print(stanza); + elseif session then + self.session.print("\n<!-- session "..session.id.." "..event_type.." -->"); + elseif event_type then + self.session.print("\n<!-- "..event_type.." -->"); + 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 +2068,10 @@ function def_env.debug:events(host, event) 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 +2125,70 @@ function def_env.debug:timers() 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 + + 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 = {}; +def_env.stats = new_section("Commands to show internal statistics"); local short_units = { seconds = "s", @@ -1913,14 +2427,19 @@ local function new_stats_context(self) 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 metric_registry = statsman.get_metric_registry(); + if not metric_registry then + return nil, [[Statistics disabled. Try `statistics = "internal"` in the global section of the config file and restart.]]; + end local collect = statsman.collect if collect then -- force collection if in manual mode collect() end - local metric_registry = statsman.get_metric_registry(); local displayed_stats = new_stats_context(self); for family_name, metric_family in iterators.sorted_pairs(metric_registry:get_metric_families()) do if not name_filter or family_name:match(name_filter) then @@ -1934,6 +2453,176 @@ function def_env.stats:show(name_filter) 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 "<unknown module>"; + 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 --git a/plugins/mod_admin_socket.lua b/plugins/mod_admin_socket.lua index 157e746c..ad6aa5d7 100644 --- a/plugins/mod_admin_socket.lua +++ b/plugins/mod_admin_socket.lua @@ -8,7 +8,7 @@ if have_unix and type(unix) == "function" then -- 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 @@ if not have_unix or type(unix) ~= "table" then 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 --git a/plugins/mod_admin_telnet.lua b/plugins/mod_admin_telnet.lua index 15220ec9..e93f61a9 100644 --- a/plugins/mod_admin_telnet.lua +++ b/plugins/mod_admin_telnet.lua @@ -12,8 +12,8 @@ module:depends("admin_shell"); 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 --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index c742ebb8..f54d2db9 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -6,12 +6,15 @@ -- COPYING file in the source package for more information. -- -local st, jid = require "util.stanza", require "util.jid"; +local usermanager = require "prosody.core.usermanager"; +local id = require "prosody.util.id"; +local jid = require "prosody.util.jid"; +local st = require "prosody.util.stanza"; local hosts = prosody.hosts; -local is_admin = require "core.usermanager".is_admin; function send_to_online(message, host) + host = host or module.host; local sessions; if host then sessions = { [host] = hosts[host] }; @@ -34,6 +37,29 @@ function send_to_online(message, host) return c; end +function send_to_all(message, host) + host = host or module.host; + local c = 0; + for username in usermanager.users(host) do + message.attr.to = username.."@"..host; + module:send(st.clone(message)); + c = c + 1; + end + return c; +end + +function send_to_role(message, role, host) + host = host or module.host; + local c = 0; + for _, recipient_jid in ipairs(usermanager.get_jids_with_role(role, host)) do + message.attr.to = recipient_jid; + module:send(st.clone(message)); + c = c + 1; + end + return c; +end + +module:default_permission("prosody:admin", ":send-announcement"); -- Old <message>-based jabberd-style announcement sending function handle_announcement(event) @@ -45,8 +71,8 @@ function handle_announcement(event) 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 +89,7 @@ end 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."; @@ -82,8 +108,10 @@ function announce_handler(_, data, state) local fields = announce_layout:data(data.form); module:log("info", "Sending server announcement to all online users"); - local message = st.message({type = "headline"}, fields.announcement):up() - :tag("subject"):text(fields.subject or "Announcement"); + local message = st.message({type = "headline"}, fields.announcement):up(); + if fields.subject and fields.subject ~= "" then + message:text_tag("subject", fields.subject); + end local count = send_to_online(message, data.to); @@ -99,3 +127,57 @@ local adhoc_new = module:require "adhoc".new; local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin"); module:provides("adhoc", announce_desc); +module:add_item("shell-command", { + section = "announce"; + section_desc = "Broadcast announcements to users"; + name = "all"; + desc = "Send announcement to all users on the host"; + args = { + { name = "host", type = "string" }; + { name = "text", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, text) --luacheck: ignore 212/self + local msg = st.message({ from = host, id = id.short() }) + :text_tag("body", text); + local count = send_to_all(msg, host); + return true, ("Announcement sent to %d users"):format(count); + end; +}); + +module:add_item("shell-command", { + section = "announce"; + section_desc = "Broadcast announcements to users"; + name = "online"; + desc = "Send announcement to all online users on the host"; + args = { + { name = "host", type = "string" }; + { name = "text", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, text) --luacheck: ignore 212/self + local msg = st.message({ from = host, id = id.short(), type = "headline" }) + :text_tag("body", text); + local count = send_to_online(msg, host); + return true, ("Announcement sent to %d users"):format(count); + end; +}); + +module:add_item("shell-command", { + section = "announce"; + section_desc = "Broadcast announcements to users"; + name = "role"; + desc = "Send announcement to users with a specific role on the host"; + args = { + { name = "host", type = "string" }; + { name = "role", type = "string" }; + { name = "text", type = "string" }; + }; + host_selector = "host"; + handler = function(self, host, role, text) --luacheck: ignore 212/self + local msg = st.message({ from = host, id = id.short() }) + :text_tag("body", text); + local count = send_to_role(msg, role, host); + return true, ("Announcement sent to %d users"):format(count); + end; +}); diff --git a/plugins/mod_auth_anonymous.lua b/plugins/mod_auth_anonymous.lua index 90646e71..21373698 100644 --- a/plugins/mod_auth_anonymous.lua +++ b/plugins/mod_auth_anonymous.lua @@ -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 --git a/plugins/mod_auth_insecure.lua b/plugins/mod_auth_insecure.lua index dc5ee616..133c3292 100644 --- a/plugins/mod_auth_insecure.lua +++ b/plugins/mod_auth_insecure.lua @@ -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 @@ function provider.set_password(username, password) 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 @@ function provider.user_exists(username) 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 --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index cf851eef..806eb9bd 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -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 = {}; @@ -36,6 +37,9 @@ local provider = {}; function provider.test_password(username, password) log("debug", "test password for user '%s'", username); local credentials = accounts:get(username) or {}; + if credentials.disabled then + return nil, "Account disabled."; + end password = saslprep(password); if not password then return nil, "Password fails SASLprep."; @@ -86,11 +90,22 @@ function provider.set_password(username, password) 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 +115,36 @@ function provider.user_exists(username) 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 +155,8 @@ function provider.create_user(username, password) 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 +166,8 @@ end 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 +184,9 @@ function provider.get_sasl_handler() 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 --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua index 8a50e820..6cced803 100644 --- a/plugins/mod_auth_internal_plain.lua +++ b/plugins/mod_auth_internal_plain.lua @@ -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; @@ -22,6 +22,9 @@ local provider = {}; function provider.test_password(username, password) log("debug", "test password for user '%s'", username); local credentials = accounts:get(username) or {}; + if credentials.disabled then + return nil, "Account disabled."; + end password = saslprep(password); if not password then return nil, "Password fails SASLprep."; @@ -48,11 +51,21 @@ function provider.set_password(username, password) 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 +80,18 @@ function provider.users() 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 --git a/plugins/mod_auth_ldap.lua b/plugins/mod_auth_ldap.lua index 4d484aaa..569cef6b 100644 --- a/plugins/mod_auth_ldap.lua +++ b/plugins/mod_auth_ldap.lua @@ -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_server = module:get_option_string("ldap_server", "localhost"); 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 @@ else 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 --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 17687959..07091a04 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -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 = {}; + +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; +}; + +-- 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 +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 + +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 -local admin_role = { ["prosody:admin"] = true }; +-- 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 get_user_roles(user) - if config_admin_jids:contains(user.."@"..host) then - return admin_role; +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 - return role_store:get(user); + role_map_store:set(user, role_name, true); end -function set_user_roles(user, roles) - role_store:set(user, roles) - return true; +function remove_user_secondary_role(user, role_name) + role_map_store:set(user, role_name, nil); 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) +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 - return false; +function set_jid_role(jid, role_name) -- luacheck: ignore 212 + return false, "not-implemented"; 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 "<none given>")); + print(" Hint: try 'migrate'?"); + end +end diff --git a/plugins/mod_blocklist.lua b/plugins/mod_blocklist.lua index 6b8ce16c..6587c8b1 100644 --- a/plugins/mod_blocklist.lua +++ b/plugins/mod_blocklist.lua @@ -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 @@ local function set_blocklist(username, blocklist) 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 @@ local function migrate_privacy_list(username) 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 @@ local function get_blocklist(username) 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 @@ module:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event) 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 @@ local function edit_blocklist(event) 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 @@ module:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist, -1); -- 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 @@ module:hook("iq-error/self/blocklist-push", function (event) 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]; @@ -262,7 +262,20 @@ local function drop_stanza(event) local to, from = attr.to, attr.from; to = to and jid_split(to); if to and from then - return is_blocked(to, from); + if is_blocked(to, from) then + return true; + end + + -- Check mediated MUC inviter + if stanza.name == "message" then + local invite = stanza:find("{http://jabber.org/protocol/muc#user}x/invite"); + if invite then + from = jid_prep(invite.attr.from); + if is_blocked(to, from) then + return true; + end + end + end end end diff --git a/plugins/mod_bookmarks.lua b/plugins/mod_bookmarks.lua index 73ea5d41..e6e74f56 100644 --- a/plugins/mod_bookmarks.lua +++ b/plugins/mod_bookmarks.lua @@ -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 --git a/plugins/mod_bosh.lua b/plugins/mod_bosh.lua index 11bfb51d..091a7d81 100644 --- a/plugins/mod_bosh.lua +++ b/plugins/mod_bosh.lua @@ -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_HOLD = 1; 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 @@ function stream_callbacks.streamopened(context, attr) 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 @@ function stream_callbacks.streamopened(context, attr) 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 @@ function module.add_host(module) }); 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 --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index c8f54fa7..e29ea6a0 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -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,16 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; 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 advertised_idle_timeout = 14*60; -- default in all net.server implementations +local network_settings = module:get_option("network_settings"); +if type(network_settings) == "table" and type(network_settings.read_timeout) == "number" then + advertised_idle_timeout = network_settings.read_timeout; +end local measure_connections = module:metric("gauge", "connections", "", "Established c2s connections", {"host", "type", "ip_family"}); @@ -39,6 +45,7 @@ local hosts = prosody.hosts; local stream_callbacks = { default_ns = "jabber:client" }; local listener = {}; local runner_callbacks = {}; +local session_events = {}; local m_tls_params = module:metric( "counter", "encrypted", "", @@ -70,11 +77,11 @@ local stream_xmlns_attr = {xmlns='urn:ietf:params:xml:ns:xmpp-streams'}; function stream_callbacks.streamopened(session, attr) -- run _streamopened in async context - session.thread:run({ stream = "opened", attr = attr }); + session.thread:run({ event = "streamopened", attr = attr }); end -function stream_callbacks._streamopened(session, attr) - local send = session.send; +function session_events.streamopened(session, event) + local send, attr = session.send, event.attr; if not attr.to then session:close{ condition = "improper-addressing", text = "A 'to' attribute is required on stream headers" }; @@ -117,8 +124,7 @@ function stream_callbacks._streamopened(session, attr) 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 +135,19 @@ function stream_callbacks._streamopened(session, attr) 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 or advertised_idle_timeout then + features:reset(); + local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }); + if stanza_size_limit then + limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); + end + if advertised_idle_timeout then + limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); + end + limits:reset(); + end send(features); else if session.secure then @@ -146,14 +163,19 @@ end function stream_callbacks.streamclosed(session, attr) -- run _streamclosed in async context - session.thread:run({ stream = "closed", attr = attr }); + session.thread:run({ event = "streamclosed", attr = attr }); end -function stream_callbacks._streamclosed(session) +function session_events.streamclosed(session) session.log("debug", "Received </stream:stream>"); session:close(false); end +function session_events.callback(session, event) + session.log("debug", "Running session callback %s", event.name); + event.callback(session, event); +end + function stream_callbacks.error(session, error, data) if error == "no-stream" then session.log("debug", "Invalid opening stream header (%s)", (data:gsub("^([^\1]+)\1", "{%1}"))); @@ -248,6 +270,9 @@ end 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 +285,18 @@ local function disconnect_user_sessions(reason, leave_resource) 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 +328,10 @@ function listener.onconnect(conn) 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; @@ -321,13 +356,11 @@ function listener.onconnect(conn) session.stream:reset(); end - session.thread = runner(function (stanza) - if st.is_stanza(stanza) then - core_process_stanza(session, stanza); - elseif stanza.stream == "opened" then - stream_callbacks._streamopened(session, stanza.attr); - elseif stanza.stream == "closed" then - stream_callbacks._streamclosed(session, stanza.attr); + session.thread = runner(function (item) + if st.is_stanza(item) then + core_process_stanza(session, item); + else + session_events[item.event](session, item); end end, runner_callbacks, session); @@ -354,11 +387,13 @@ function listener.onconnect(conn) 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 +461,7 @@ module:hook("c2s-read-timeout", keepalive, -1); 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 --git a/plugins/mod_carbons.lua b/plugins/mod_carbons.lua index 7a5b757c..3fa34be7 100644 --- a/plugins/mod_carbons.lua +++ b/plugins/mod_carbons.lua @@ -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 --git a/plugins/mod_component.lua b/plugins/mod_component.lua index f57c4381..86ceb980 100644 --- a/plugins/mod_component.lua +++ b/plugins/mod_component.lua @@ -10,16 +10,16 @@ module:set_global(); 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 hosts = prosody.hosts; 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 @@ function module.add_host(module) 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 @@ function stream_callbacks.handlestanza(session, stanza) 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 --git a/plugins/mod_cron.lua b/plugins/mod_cron.lua index 33d97df6..29c1aa93 100644 --- a/plugins/mod_cron.lua +++ b/plugins/mod_cron.lua @@ -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 @@ function module.add_host(host_module) 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 module.add_host(host_module) 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 --git a/plugins/mod_csi.lua b/plugins/mod_csi.lua index 458ff491..76a5afd4 100644 --- a/plugins/mod_csi.lua +++ b/plugins/mod_csi.lua @@ -1,10 +1,12 @@ -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"}); +local count = module:metric("gauge", "state", "sessions", "", { "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 +15,7 @@ function refire_event(name) 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 @@ -22,14 +25,22 @@ end 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"); +module:hook_global("stats-update", function() + local sessions = prosody.hosts[module.host].sessions; + if not sessions then return end + local active, inactive, flushing = 0, 0, 0; + for _, user_session in pairs(sessions) do + for _, session in pairs(user_session.sessions) do + if session.state == "inactive" then + inactive = inactive + 1; + elseif session.state == "active" then + active = active + 1; + elseif session.state == "flushing" then + flushing = flushing + 1; + end + end end -end -module:hook("module-loaded", module.load); -module:hook("module-unloaded", module.load); + count:with_labels("active"):set(active); + count:with_labels("inactive"):set(inactive); + count:with_labels("flushing"):set(flushing); +end); diff --git a/plugins/mod_csi_simple.lua b/plugins/mod_csi_simple.lua index fdd1fd6c..379371ef 100644 --- a/plugins/mod_csi_simple.lua +++ b/plugins/mod_csi_simple.lua @@ -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 @@ local flush_reasons = module:metric( { "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 @@ local function manage_buffer(stanza, session) 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 @@ local function flush_buffer(data, session) 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 @@ function module.command(arg) 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 --git a/plugins/mod_debug_reset.lua b/plugins/mod_debug_reset.lua new file mode 100644 index 00000000..5964aff0 --- /dev/null +++ b/plugins/mod_debug_reset.lua @@ -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 --git a/plugins/mod_debug_stanzas/watcher.lib.lua b/plugins/mod_debug_stanzas/watcher.lib.lua new file mode 100644 index 00000000..1e673648 --- /dev/null +++ b/plugins/mod_debug_stanzas/watcher.lib.lua @@ -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 --git a/plugins/mod_dialback.lua b/plugins/mod_dialback.lua index 66082333..a0a8bcb9 100644 --- a/plugins/mod_dialback.lua +++ b/plugins/mod_dialback.lua @@ -10,12 +10,12 @@ local hosts = _G.hosts; 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 --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 9d2991bf..3517344d 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -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 @@ module:hook("s2s-stream-features", function (event) 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 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function( 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 --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua index ae418fd8..ade1e327 100644 --- a/plugins/mod_external_services.lua +++ b/plugins/mod_external_services.lua @@ -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 --git a/plugins/mod_groups.lua b/plugins/mod_groups.lua index 0c44f481..1a31c51f 100644 --- a/plugins/mod_groups.lua +++ b/plugins/mod_groups.lua @@ -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 --git a/plugins/mod_http.lua b/plugins/mod_http.lua index 0cee26c4..c13a2363 100644 --- a/plugins/mod_http.lua +++ b/plugins/mod_http.lua @@ -11,24 +11,26 @@ pcall(function () 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_methods = module:get_option_set("access_control_allow_methods", { "GET 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 @@ end 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 @@ function moduleapi.http_url(module, app_name, default_path) 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 @@ function moduleapi.http_url(module, app_name, default_path) 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 @@ module.add_host(module); -- set up handling on global context too 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 @@ end 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 @@ local function get_forwarded_connection_info(request) --> ip:string, secure:bool return ip, secure; end +end module:wrap_object_event(server._events, false, function (handlers, event_name, event_data) local request = event_data.request; diff --git a/plugins/mod_http_errors.lua b/plugins/mod_http_errors.lua index ec54860c..c92e44ce 100644 --- a/plugins/mod_http_errors.lua +++ b/plugins/mod_http_errors.lua @@ -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 @@ local html = [[ <meta charset="utf-8"> <title>{title}</title> <style> -body{margin-top:14%;text-align:center;background-color:#f8f8f8;font-family:sans-serif} +:root{color-scheme:light dark} +body{margin-top:14%;text-align:center;font-family:sans-serif} h1{font-size:xx-large} p{font-size:x-large} p.warning>span{font-size:large;background-color:yellow} p.extra{font-size:large;font-family:courier} @media(prefers-color-scheme:dark){ -body{background-color:#161616;color:#eee} p.warning>span{background-color:inherit;color:yellow} } </style> diff --git a/plugins/mod_http_file_share.lua b/plugins/mod_http_file_share.lua index b6200628..48972067 100644 --- a/plugins/mod_http_file_share.lua +++ b/plugins/mod_http_file_share.lua @@ -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 uploads = module:open_store("uploads", "archive"); local persist_stats = module:open_store("upload_stats", "map"); -- id, <request>, 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 @@ -135,7 +138,7 @@ end 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 @@ function may_upload(uploader, filename, filesize, filetype) -- > boolean, error 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 @@ end 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); @@ -450,11 +452,11 @@ function handle_download(event, path) -- GET /uploads/:slot+filename return response:send_file(handle); end -if expiry >= 0 and not external_base_url then +if expiry < math.huge 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 --git a/plugins/mod_http_files.lua b/plugins/mod_http_files.lua index b921116a..799fb9c8 100644 --- a/plugins/mod_http_files.lua +++ b/plugins/mod_http_files.lua @@ -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 @@ function serve(opts) 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 --git a/plugins/mod_http_openmetrics.lua b/plugins/mod_http_openmetrics.lua index 0c204ff4..5f151521 100644 --- a/plugins/mod_http_openmetrics.lua +++ b/plugins/mod_http_openmetrics.lua @@ -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 --git a/plugins/mod_invites.lua b/plugins/mod_invites.lua index 88690f7e..5ee9430a 100644 --- a/plugins/mod_invites.lua +++ b/plugins/mod_invites.lua @@ -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 @@ function use(token) --luacheck: ignore 131/use 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"); - - -- 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); +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"; + + 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 - - 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); + end; +}); + +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 + return subcommands[cmd](arg); +end - local sm = require "core.storagemanager"; - local mm = require "core.modulemanager"; +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 host = arg[1]; - assert(prosody.hosts[host], "Host "..tostring(host).." does not exist"); + 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 @@ function module.command(arg) 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 @@ function module.command(arg) 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 --git a/plugins/mod_invites_adhoc.lua b/plugins/mod_invites_adhoc.lua index 9c0660e9..3ef4116d 100644 --- a/plugins/mod_invites_adhoc.lua +++ b/plugins/mod_invites_adhoc.lua @@ -1,8 +1,8 @@ -- 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 adhocutil = require "prosody.util.adhoc"; local new_adhoc = module:require("adhoc").new; @@ -13,8 +13,7 @@ local allow_user_invites = module:get_option_boolean("allow_user_invites", false -- 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 +41,8 @@ local invite_result_form = dataforms.new({ -- 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 +62,7 @@ module:provides("adhoc", new_adhoc("Create new contact invite", "urn:xmpp:invite }; }; 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 @@ -128,3 +99,32 @@ module:provides("adhoc", new_adhoc("Create new account invite", "urn:xmpp:invite }; }; end, "admin")); + +local password_reset_form = dataforms.new({ + title = "Generate Password Reset Invite"; + { + name = "accountjid"; + type = "jid-single"; + required = true; + label = "The XMPP ID for the account to generate a password reset invite for"; + }; +}); + +module:provides("adhoc", new_adhoc("Create password reset invite", "xmpp:prosody.im/mod_invites_adhoc#password-reset", + adhocutil.new_simple_form(password_reset_form, + function (fields, err) + if err then return { status = "completed"; error = { message = "Fill in the form correctly" } }; end + local username = split_jid(fields.accountjid); + local invite = invites.create_account_reset(username); + return { + status = "completed"; + result = { + layout = invite_result_form; + values = { + uri = invite.uri; + url = invite.landing_page; + expire = datetime.datetime(invite.expires); + }; + }; + }; + end), "admin")); diff --git a/plugins/mod_invites_register.lua b/plugins/mod_invites_register.lua index d1d801ad..d9274ce4 100644 --- a/plugins/mod_invites_register.lua +++ b/plugins/mod_invites_register.lua @@ -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 @@ module:hook("user-registered", function (event) 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 --git a/plugins/mod_iq.lua b/plugins/mod_iq.lua index 87c3a467..77969147 100644 --- a/plugins/mod_iq.lua +++ b/plugins/mod_iq.lua @@ -7,7 +7,7 @@ -- -local st = require "util.stanza"; +local st = require "prosody.util.stanza"; local full_sessions = prosody.full_sessions; diff --git a/plugins/mod_lastactivity.lua b/plugins/mod_lastactivity.lua index 91d11bd2..e41bc02a 100644 --- a/plugins/mod_lastactivity.lua +++ b/plugins/mod_lastactivity.lua @@ -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 --git a/plugins/mod_legacyauth.lua b/plugins/mod_legacyauth.lua index 52f2c143..048cd3e1 100644 --- a/plugins/mod_legacyauth.lua +++ b/plugins/mod_legacyauth.lua @@ -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 --git a/plugins/mod_limits.lua b/plugins/mod_limits.lua index 4f1b618e..407f681f 100644 --- a/plugins/mod_limits.lua +++ b/plugins/mod_limits.lua @@ -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 --git a/plugins/mod_mam/mamprefs.lib.lua b/plugins/mod_mam/mamprefs.lib.lua index dd82b626..cddcbd30 100644 --- a/plugins/mod_mam/mamprefs.lib.lua +++ b/plugins/mod_mam/mamprefs.lib.lua @@ -10,12 +10,15 @@ -- -- 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); -end +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 + do -- luacheck: ignore 211/prefs_format local prefs_format = { diff --git a/plugins/mod_mam/mamprefsxml.lib.lua b/plugins/mod_mam/mamprefsxml.lib.lua index c408fbea..b325e886 100644 --- a/plugins/mod_mam/mamprefsxml.lib.lua +++ b/plugins/mod_mam/mamprefsxml.lib.lua @@ -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 --git a/plugins/mod_mam/mod_mam.lua b/plugins/mod_mam/mod_mam.lua index bebee812..b57fc030 100644 --- a/plugins/mod_mam/mod_mam.lua +++ b/plugins/mod_mam/mod_mam.lua @@ -15,36 +15,36 @@ local xmlns_delay = "urn:xmpp:delay"; 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 @@ if not archive.find then 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 @@ module:hook("iq-get/self/"..xmlns_mam..":metadata", function (event) 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 @@ module:hook("iq-get/self/"..xmlns_mam..":metadata", function (event) 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 function message_handler(event, c2s) 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 @@ module:hook("message/offline/broadcast", function (event) 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 @@ if cleanup_after ~= "never" then -- 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 @@ if cleanup_after ~= "never" then 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 --git a/plugins/mod_message.lua b/plugins/mod_message.lua index 9c07e796..aa9f5c2d 100644 --- a/plugins/mod_message.lua +++ b/plugins/mod_message.lua @@ -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 --git a/plugins/mod_mimicking.lua b/plugins/mod_mimicking.lua index ab7612cb..52a070f5 100644 --- a/plugins/mod_mimicking.lua +++ b/plugins/mod_mimicking.lua @@ -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 --git a/plugins/mod_motd.lua b/plugins/mod_motd.lua index 47e64be3..bee0820c 100644 --- a/plugins/mod_motd.lua +++ b/plugins/mod_motd.lua @@ -13,7 +13,7 @@ local motd_jid = module:get_option_string("motd_jid", host); 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 --git a/plugins/mod_muc_mam.lua b/plugins/mod_muc_mam.lua index 0918b95d..23bb7dab 100644 --- a/plugins/mod_muc_mam.lua +++ b/plugins/mod_muc_mam.lua @@ -16,28 +16,28 @@ local xmlns_st_id = "urn:xmpp:sid:0"; 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 log_by_default = module:get_option_boolean("muc_log_by_default", true); 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 function save_to_history(self, stanza) 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 @@ end); -- 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 @@ if cleanup_after ~= "never" then -- 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 @@ if cleanup_after ~= "never" then 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 --git a/plugins/mod_muc_unique.lua b/plugins/mod_muc_unique.lua index 13284745..62ec74b8 100644 --- a/plugins/mod_muc_unique.lua +++ b/plugins/mod_muc_unique.lua @@ -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 --git a/plugins/mod_net_multiplex.lua b/plugins/mod_net_multiplex.lua index ddd58463..3f5ee54d 100644 --- a/plugins/mod_net_multiplex.lua +++ b/plugins/mod_net_multiplex.lua @@ -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 --git a/plugins/mod_offline.lua b/plugins/mod_offline.lua index dffe8357..b71bbfd9 100644 --- a/plugins/mod_offline.lua +++ b/plugins/mod_offline.lua @@ -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 --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index 71e45e7c..33eee2ec 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -1,21 +1,23 @@ -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 rostermanager = require "prosody.core.rostermanager"; +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"; local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; +local is_contact_subscribed = rostermanager.is_contact_subscribed; + local lib_pubsub = module:require "pubsub"; local empty_set = set_new(); @@ -24,7 +26,7 @@ local empty_set = set_new(); 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 +38,7 @@ local services = cache.new(service_cache_size, function (username, _) 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 +51,7 @@ local host = module.host; 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 @@ -84,6 +86,7 @@ function check_node_config(node, actor, new_config) -- luacheck: ignore 212/node return false; end if new_config["access_model"] ~= "presence" + and new_config["access_model"] ~= "roster" and new_config["access_model"] ~= "whitelist" and new_config["access_model"] ~= "open" then return false; @@ -136,10 +139,14 @@ end 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 <items> container @@ -151,6 +158,9 @@ local function get_broadcaster(username) 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 @@ -249,6 +259,20 @@ function get_pep_service(username) end return "outcast"; end; + roster = function (jid, node) + jid = jid_bare(jid); + local allowed_groups = set_new(node.config.roster_groups_allowed); + local roster = rostermanager.load_roster(username, host); + if not roster[jid] then + return "outcast"; + end + for group in pairs(roster[jid].groups) do + if allowed_groups:contains(group) then + return "member"; + end + end + return "outcast"; + end; }; jid = user_bare; @@ -306,7 +330,7 @@ local function resend_last_item(jid, node, service) 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 --git a/plugins/mod_pep_simple.lua b/plugins/mod_pep_simple.lua index e686b99b..a196a0ff 100644 --- a/plugins/mod_pep_simple.lua +++ b/plugins/mod_pep_simple.lua @@ -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 --git a/plugins/mod_ping.lua b/plugins/mod_ping.lua index b6ccc928..018f815a 100644 --- a/plugins/mod_ping.lua +++ b/plugins/mod_ping.lua @@ -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 --git a/plugins/mod_posix.lua b/plugins/mod_posix.lua index 7f048be3..101e6e62 100644 --- a/plugins/mod_posix.lua +++ b/plugins/mod_posix.lua @@ -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 --git a/plugins/mod_presence.lua b/plugins/mod_presence.lua index 3f9a0c12..f939fa00 100644 --- a/plugins/mod_presence.lua +++ b/plugins/mod_presence.lua @@ -15,19 +15,19 @@ local tonumber = tonumber; 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 --git a/plugins/mod_private.lua b/plugins/mod_private.lua index 6046d490..2359494c 100644 --- a/plugins/mod_private.lua +++ b/plugins/mod_private.lua @@ -7,7 +7,7 @@ -- -local st = require "util.stanza" +local st = require "prosody.util.stanza" local private_storage = module:open_store("private", "map"); diff --git a/plugins/mod_proxy65.lua b/plugins/mod_proxy65.lua index 069ce0a9..38acc79a 100644 --- a/plugins/mod_proxy65.lua +++ b/plugins/mod_proxy65.lua @@ -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 --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua index ef31f326..c17d9e63 100644 --- a/plugins/mod_pubsub/mod_pubsub.lua +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -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 xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; 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 @@ end -- 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 @@ function simple_broadcast(kind, node, jids, item, actor, node_obj, service) --lu 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 @@ end -- 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,23 @@ module:hook("host-disco-items", function (event) end end); -local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); -local function get_affiliation(jid) +local admin_aff = module:get_option_enum("default_admin_affiliation", "owner", "publisher", "member", "outcast", "none"); + +module:default_permission("prosody:admin", ":service-admin"); +module:default_permission("prosody:admin", ":create-node"); + +local function get_affiliation(jid, _, action) 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 then + -- The host itself (i.e. local modules) is treated as an admin. + -- Check this first as to avoid sendig a host JID to :may() + return admin_aff; + end + if action == "create" and module:may(":create-node", bare_jid) then + -- Only one affiliation is allowed to create nodes by default + return "owner"; + end + if module:may(":service-admin", bare_jid) then return admin_aff; end end @@ -192,7 +218,7 @@ function set_service(new_service) 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 +245,7 @@ function module.load() 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; @@ -230,9 +256,59 @@ function module.load() broadcaster = simple_broadcast; itemcheck = is_item_stanza; check_node_config = check_node_config; + metadata_subset = { + "title"; + "description"; + "payload_type"; + "access_model"; + "publish_model"; + }; get_affiliation = get_affiliation; jid = module.host; 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 --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua index 3196569f..f4d44f36 100644 --- a/plugins/mod_pubsub/pubsub.lib.lua +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -1,13 +1,12 @@ -local t_unpack = table.unpack or unpack; -- luacheck: ignore 113 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"; @@ -18,7 +17,7 @@ local _M = {}; local handlers = {}; _M.handlers = handlers; -local pubsub_errors = { +local pubsub_errors = errors.init("pubsub", xmlns_pubsub_errors, { ["conflict"] = { "cancel", "conflict" }; ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; @@ -33,16 +32,13 @@ local pubsub_errors = { ["precondition-not-met"] = { "cancel", "conflict", nil, "precondition-not-met" }; ["invalid-item"] = { "modify", "bad-request", "invalid item" }; ["persistent-items-unsupported"] = { "cancel", "feature-not-implemented", nil, "persistent-items" }; -}; -local function pubsub_error_reply(stanza, error) - local e = pubsub_errors[error]; - if not e and errors.is_err(error) then - e = { error.type, error.condition, error.text, error.pubsub_condition }; - end - local reply = st.error_reply(stanza, t_unpack(e, 1, 3)); - if e[4] then - reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); +}); +local function pubsub_error_reply(stanza, error, context) + local err = pubsub_errors.wrap(error, context); + if error == "precondition-not-met" and type(context) == "table" and type(context.field) == "string" then + err.text = "Field does not match: " .. context.field; end + local reply = st.error_reply(stanza, err); return reply; end _M.pubsub_error_reply = pubsub_error_reply; @@ -110,6 +106,12 @@ local node_config_form = dataform { }; }; { + type = "list-multi"; -- TODO some way to inject options + name = "roster_groups_allowed"; + var = "pubsub#roster_groups_allowed"; + label = "Roster groups allowed to subscribe"; + }; + { type = "list-single"; name = "publish_model"; var = "pubsub#publish_model"; @@ -164,6 +166,17 @@ local node_config_form = dataform { 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; @@ -189,23 +202,28 @@ local node_metadata_form = dataform { }; { type = "text-single"; - name = "pubsub#title"; + name = "title"; + var = "pubsub#title"; }; { type = "text-single"; - name = "pubsub#description"; + name = "description"; + var = "pubsub#description"; }; { type = "text-single"; - name = "pubsub#type"; + name = "payload_type"; + var = "pubsub#type"; }; { type = "text-single"; - name = "pubsub#access_model"; + name = "access_model"; + var = "pubsub#access_model"; }; { type = "text-single"; - name = "pubsub#publish_model"; + name = "publish_model"; + var = "pubsub#publish_model"; }; }; _M.node_metadata_form = node_metadata_form; @@ -277,27 +295,14 @@ end function _M.handle_disco_info_node(event, service) local stanza, reply, node = event.stanza, event.reply, event.node; - local ok, ret = service:get_nodes(stanza.attr.from); + local ok, meta = service:get_node_metadata(node, stanza.attr.from); if not ok then - event.origin.send(pubsub_error_reply(stanza, ret)); - return true; - end - local node_obj = ret[node]; - if not node_obj then - event.origin.send(pubsub_error_reply(stanza, "item-not-found")); + event.origin.send(pubsub_error_reply(stanza, meta)); return true; end event.exists = true; reply:tag("identity", { category = "pubsub", type = "leaf" }):up(); - if node_obj.config then - reply:add_child(node_metadata_form:form({ - ["pubsub#title"] = node_obj.config.title; - ["pubsub#description"] = node_obj.config.description; - ["pubsub#type"] = node_obj.config.payload_type; - ["pubsub#access_model"] = node_obj.config.access_model; - ["pubsub#publish_model"] = node_obj.config.publish_model; - }, "result")); - end + reply:add_child(node_metadata_form:form(meta, "result")); end function _M.handle_disco_items_node(event, service) @@ -347,6 +352,13 @@ function handlers.get_items(origin, stanza, items, service) 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 +370,9 @@ function handlers.get_items(origin, stanza, items, service) 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); @@ -658,7 +673,7 @@ function handlers.set_publish(origin, stanza, publish, service) if item then item.attr.publisher = service.config.normalize_jid(stanza.attr.from); end - local ok, ret = service:publish(node, stanza.attr.from, id, item, required_config); + local ok, ret, context = service:publish(node, stanza.attr.from, id, item, required_config); local reply; if ok then if type(ok) == "string" then @@ -669,7 +684,7 @@ function handlers.set_publish(origin, stanza, publish, service) :tag("publish", { node = node }) :tag("item", { id = id }); else - reply = pubsub_error_reply(stanza, ret); + reply = pubsub_error_reply(stanza, ret, context); end origin.send(reply); return true; @@ -678,8 +693,7 @@ end 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 --git a/plugins/mod_register_ibr.lua b/plugins/mod_register_ibr.lua index 8042de7e..ee47a1e0 100644 --- a/plugins/mod_register_ibr.lua +++ b/plugins/mod_register_ibr.lua @@ -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 additional_fields = module:get_option("additional_registration_fields", {}); +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_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 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event) 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 @@ module:hook("stanza/iq/jabber:iq:register:query", function(event) 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 --git a/plugins/mod_register_limits.lua b/plugins/mod_register_limits.lua index cb430f7f..e127bb86 100644 --- a/plugins/mod_register_limits.lua +++ b/plugins/mod_register_limits.lua @@ -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 --git a/plugins/mod_roster.lua b/plugins/mod_roster.lua index 37fa197a..82016d27 100644 --- a/plugins/mod_roster.lua +++ b/plugins/mod_roster.lua @@ -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"); @@ -56,7 +56,7 @@ module:hook("iq/self/jabber:iq:roster:query", function(event) roster:up(); -- move out from item end end - roster.tags[1].attr.ver = server_ver; + roster.tags[1].attr.ver = tostring(server_ver); end session.send(roster); session.interested = true; -- resource is interested in roster updates diff --git a/plugins/mod_s2s.lua b/plugins/mod_s2s.lua index ee65ba70..8eb1565e 100644 --- a/plugins/mod_s2s.lua +++ b/plugins/mod_s2s.lua @@ -13,35 +13,42 @@ local hosts = prosody.hosts; local core_process_stanza = prosody.core_process_stanza; local tostring, type = tostring, type; -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_timeout = module:get_option_number("s2s_timeout", 90); -local stream_close_timeout = module:get_option_number("s2s_close_timeout", 5); +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 queue = require "prosody.util.queue"; + +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 sendq_size = module:get_option_integer("s2s_send_queue_size", 1024*32, 1); + +local advertised_idle_timeout = 14*60; -- default in all net.server implementations +local network_settings = module:get_option("network_settings"); +if type(network_settings) == "table" and type(network_settings.read_timeout) == "number" then + advertised_idle_timeout = network_settings.read_timeout; +end local measure_connections_inbound = module:metric( "gauge", "connections_inbound", "", @@ -82,6 +89,7 @@ local m_tls_params = module:metric( local sessions = module:shared("sessions"); local runner_callbacks = {}; +local session_events = {}; local listener = {}; @@ -95,6 +103,12 @@ local s2s_service_options = { }; 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() @@ -122,7 +136,7 @@ local bouncy_stanzas = { message = true, presence = true, iq = true }; local function bounce_sendq(session, reason) local sendq = session.sendq; if not sendq then return; end - session.log("info", "Sending error replies for %d queued stanzas because of failed outgoing connection to %s", #sendq, session.to_host); + session.log("info", "Sending error replies for %d queued stanzas because of failed outgoing connection to %s", sendq.count(), session.to_host); local dummy = { type = "s2sin"; send = function () @@ -141,24 +155,23 @@ local function bounce_sendq(session, reason) if session.had_stream then -- set when a stream is opened by the remote error_type, condition = "wait", "remote-server-timeout"; end - if errors.is_err(reason) then + if errors.is_error(reason) then error_type, condition, reason_text = reason.type, reason.condition, reason.text; 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 stanza in sendq:consume() 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 session.sendq = nil; end @@ -182,15 +195,14 @@ function route_to_existing_session(event) (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); - else + if not host.sendq then -- luacheck: ignore 122 - host.sendq = { queued_item }; + host.sendq = queue.new(sendq_size); + end + if not host.sendq:push(st.clone(stanza)) then + host.log("warn", "stanza [%s] not queued ", stanza.name); + event.origin.send(st.error_reply(stanza, "wait", "resource-constraint", "Outgoing stanza queue full")); + return true; end host.log("debug", "stanza [%s] queued ", stanza.name); return true; @@ -215,7 +227,8 @@ function route_to_new_session(event) -- 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 = queue.new(sendq_size); + host_session.sendq:push(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 +268,37 @@ function module.add_host(module) 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 not (stanza_size_limit or advertised_idle_timeout) then return end + local limits = event.features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }) + if stanza_size_limit then + limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); + end + if advertised_idle_timeout then + limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); + end + limits:up(); + end); + module:hook_tag("urn:xmpp:bidi", "bidi", function(session, stanza) + -- Advertising features on bidi connections where no <stream:features> 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 +324,7 @@ function module.add_host(module) 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 @@ -326,11 +367,11 @@ function mark_connected(session) if session.direction == "outgoing" then if sendq then - session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", #sendq, session.to_host); + session.log("debug", "sending %d queued stanzas across new outgoing connection to %s", sendq.count(), session.to_host); local send = session.sends2s; - for i, data in ipairs(sendq) do - send(data[1]); - sendq[i] = nil; + for stanza in sendq:consume() do + -- TODO check send success + send(stanza); end session.sendq = nil; end @@ -393,10 +434,10 @@ end --- 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 +449,7 @@ local function session_secure(session) 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; @@ -430,15 +470,17 @@ local xmlns_xmpp_streams = "urn:ietf:params:xml:ns:xmpp-streams"; function stream_callbacks.streamopened(session, attr) -- run _streamopened in async context - session.thread:run({ stream = "opened", attr = attr }); + session.thread:run({ event = "streamopened", attr = attr }); end -function stream_callbacks._streamopened(session, attr) +function session_events.streamopened(session, event) + local attr = event.attr; session.version = tonumber(attr.version) or 0; 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 +568,18 @@ function stream_callbacks._streamopened(session, attr) end if ( session.type == "s2sin" or session.type == "s2sout" ) or features.tags[1] then + if stanza_size_limit or advertised_idle_timeout then + features:reset(); + local limits = features:tag("limits", { xmlns = "urn:xmpp:stream-limits:0" }); + if stanza_size_limit then + limits:text_tag("max-bytes", string.format("%d", stanza_size_limit)); + end + if advertised_idle_timeout then + limits:text_tag("idle-seconds", string.format("%d", advertised_idle_timeout)); + end + features:reset(); + end + log("debug", "Sending stream features: %s", features); session.sends2s(features); else @@ -561,14 +615,19 @@ function stream_callbacks._streamopened(session, attr) end end -function stream_callbacks._streamclosed(session) +function session_events.streamclosed(session) (session.log or log)("debug", "Received </stream:stream>"); session:close(false); end +function session_events.callback(session, event) + session.log("debug", "Running session callback %s", event.name); + event.callback(session, event); +end + function stream_callbacks.streamclosed(session, attr) -- run _streamclosed in async context - session.thread:run({ stream = "closed", attr = attr }); + session.thread:run({ event = "streamclosed", attr = attr }); end -- Some stream conditions indicate a problem on our end, e.g. that we sent @@ -732,13 +791,11 @@ end local function initialize_session(session) local stream = new_xmpp_stream(session, stream_callbacks, stanza_size_limit); - session.thread = runner(function (stanza) - if st.is_stanza(stanza) then - core_process_stanza(session, stanza); - elseif stanza.stream == "opened" then - stream_callbacks._streamopened(session, stanza.attr); - elseif stanza.stream == "closed" then - stream_callbacks._streamclosed(session, stanza.attr); + session.thread = runner(function (item) + if st.is_stanza(item) then + core_process_stanza(session, item); + else + session_events[item.event](session, item); end end, runner_callbacks, session); @@ -760,6 +817,7 @@ local function initialize_session(session) local w = conn.write; if conn:ssl() then + -- Direct TLS was used session_secure(session); end @@ -770,6 +828,11 @@ local function initialize_session(session) 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 @@ -932,14 +995,27 @@ end -- Complete the sentence "Your certificate " with what's wrong local function friendly_cert_error(session) --> string if session.cert_chain_status == "invalid" then - if session.cert_chain_errors then + if type(session.cert_chain_errors) == "table" then local cert_errors = set.new(session.cert_chain_errors[1]); if cert_errors:contains("certificate has expired") then 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 + -- TODO cert_chain_errors can be a string, handle that return "is not trusted"; -- for some other reason elseif session.cert_identity_status == "invalid" then return "is not valid for this name"; @@ -966,6 +1042,8 @@ function check_auth_policy(event) -- 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 +1054,7 @@ module:hook("s2s-check-certificate", check_auth_policy, -1); 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 --git a/plugins/mod_s2s_auth_certs.lua b/plugins/mod_s2s_auth_certs.lua index 992ee934..2517c95f 100644 --- a/plugins/mod_s2s_auth_certs.lua +++ b/plugins/mod_s2s_auth_certs.lua @@ -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 @@ local measure_cert_statuses = module:metric("counter", "checked", "", "Certifica 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 @@ module:hook("s2s-check-certificate", function(event) 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 --git a/plugins/mod_s2s_auth_dane_in.lua b/plugins/mod_s2s_auth_dane_in.lua new file mode 100644 index 00000000..9167e8a9 --- /dev/null +++ b/plugins/mod_s2s_auth_dane_in.lua @@ -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 --git a/plugins/mod_s2s_bidi.lua b/plugins/mod_s2s_bidi.lua index addcd6e2..8588ce59 100644 --- a/plugins/mod_s2s_bidi.lua +++ b/plugins/mod_s2s_bidi.lua @@ -5,17 +5,22 @@ -- 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"; local require_encryption = module:get_option_boolean("s2s_require_encryption", true); +local offers_sent = module:metric("counter", "offers_sent", "", "Bidirectional connection offers sent", {}); +local offers_recv = module:metric("counter", "offers_recv", "", "Bidirectional connection offers received", {}); +local offers_taken = module:metric("counter", "offers_taken", "", "Bidirectional connection offers taken", {}); + module:hook("s2s-stream-features", function(event) local origin, features = event.origin, event.features; if origin.type == "s2sin_unauthed" and (not require_encryption or origin.secure) then features:tag("bidi", { xmlns = xmlns_bidi_feature }):up(); + offers_sent:with_labels():add(1); end end); @@ -25,7 +30,10 @@ module:hook_tag("http://etherx.jabber.org/streams", "features", function (sessio 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); + offers_taken:with_labels():add(1); end end end, 200); @@ -34,6 +42,7 @@ module:hook_tag("urn:xmpp:bidi", "bidi", function(session) if session.type == "s2sin_unauthed" and (not require_encryption or session.secure) then session.log("debug", "Requested bidirectional stream"); session.outgoing = true; + offers_recv:with_labels():add(1); return true; end end); diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index ab863aa3..9544e913 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -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 usermanager_get_sasl_handler = require "core.usermanager".get_sasl_handler; +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"prosody.util.x509".pem2der; +local hashes = require "prosody.util.hashes"; +local ssl = require "ssl"; -- FIXME Isolate LuaSec from the rest of the code + +local certmanager = require "prosody.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 @@ local function handle_status(session, status, ret, err_msg) 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 @@ local function sasl_process_cdata(session, stanza) 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 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:auth", function(event) 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 @@ module:hook("stanza/urn:ietf:params:xml:ns:xmpp-sasl:abort", function(event) 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,60 @@ module:hook("stream-features", function(event) 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"); + else + log("debug", "Channel binding 'tls-exporter' not supported"); + 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 +409,14 @@ module:hook("stream-features", function(event) 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 +439,7 @@ module:hook("stream-features", function(event) 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 +461,15 @@ end); 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 --git a/plugins/mod_scansion_record.lua b/plugins/mod_scansion_record.lua index 5fefd398..1ec55952 100644 --- a/plugins/mod_scansion_record.lua +++ b/plugins/mod_scansion_record.lua @@ -2,11 +2,11 @@ local names = { "Romeo", "Juliet", "Mercutio", "Tybalt", "Benvolio" }; 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 --git a/plugins/mod_server_contact_info.lua b/plugins/mod_server_contact_info.lua index 42316078..67fed752 100644 --- a/plugins/mod_server_contact_info.lua +++ b/plugins/mod_server_contact_info.lua @@ -6,21 +6,23 @@ -- 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 it = require "prosody.util.iterators"; +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 +31,17 @@ local contact_config = module:get_option("contact_info", { 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 it.sorted_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 --git a/plugins/mod_server_info.lua b/plugins/mod_server_info.lua new file mode 100644 index 00000000..5469bf02 --- /dev/null +++ b/plugins/mod_server_info.lua @@ -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 --git a/plugins/mod_smacks.lua b/plugins/mod_smacks.lua index 292b7e0b..65d410e0 100644 --- a/plugins/mod_smacks.lua +++ b/plugins/mod_smacks.lua @@ -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 @@ local resumption_age = module:metric( "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 xmlns_sm3 = "urn:xmpp:sm:3"; 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 all_old_sessions = module:open_store("smacks_h"); 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 @@ local function ack_delayed(session, stanza) 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 @@ end local function request_ack(session, reason) local queue = session.outgoing_stanza_queue; - session.log("debug", "Sending <r> (inside timer, before send) from %s - #queue=%d", reason, queue:count_unacked()); + session.log("debug", "Sending <r> 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 <r> (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 @@ local function outgoing_stanza_filter(stanza, session) -- 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 @@ module:hook("pre-session-close", function(event) 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 @@ local function wrap_session(session, resume) 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 @@ function handle_enable(session, stanza, xmlns_sm) 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 @@ module:hook_tag(xmlns_sm3, "enabled", function (session, stanza) return handle_e 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 <a> (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 function handle_unacked_stanzas(session) 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 @@ -494,16 +541,17 @@ module:hook("pre-resource-unbind", function (event) return end - prosody.main_thread:run(function () - 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() }); - 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); + session.thread:run({ + event = "callback"; + name = "mod_smacks/destroy_hibernating"; + callback = function () + session.log("debug", "Destroying session for hibernating too long"); + save_old_session(session); + session.resumption_token = nil; + sessionmanager.destroy_session(session, "Hibernating too long"); + sessions_expired(1); + end; + }); end); if session.conn then local conn = session.conn; @@ -535,131 +583,115 @@ end module:hook("s2sout-destroyed", handle_s2s_destroyed); module:hook("s2sin-destroyed", handle_s2s_destroyed); -local function get_session_id(session) - return session.id or (tostring(session):match("[a-f0-9]+$")); -end - -function handle_resume(session, stanza, xmlns_sm) +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 original_session and original_session.destroyed then + original_session.log("error", "Tried to resume a destroyed session. This should not happen! %s", debug.traceback()); + session_registry[registry_key(session, id)] = nil; + original_session = nil; + end 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!") - 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(); + return nil, enable_errors.new("expired", { h = old_session.h }); end + session.log("debug", "Tried to resume non-existent session with id %s", id); + return nil, enable_errors.new("unknown_session"); + end - 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; - - 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 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 - 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 + session.log("debug", "mod_smacks resuming existing session %s...", original_session.id); - original_session.send(st.stanza("resumed", { xmlns = xmlns_sm, - h = format_h(original_session.handled_stanza_count), previd = id })); + local queue = original_session.outgoing_stanza_queue; + local h = tonumber(stanza.attr.h); - -- Ok, we need to re-send any stanzas that the client didn't see - -- ...they are what is now left in the outgoing stanza queue - -- We have to use the send of "session" because we don't want to add our resent stanzas - -- to the outgoing queue again + original_session.log("debug", "Pre-resumption #queue = %d", queue:count_unacked()) + local acked, err = ack_errors.coerce(queue:ack(h)); -- luacheck: ignore 211/acked - 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); + 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); + + 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 + + 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()); + + -- Add our own handlers to the resumed session (filters have been reset in the update) + wrap_session(original_session, true); + + -- 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 + +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); @@ -714,8 +746,7 @@ module:hook_global("server-stopping", function(event) 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 --git a/plugins/mod_stanza_debug.lua b/plugins/mod_stanza_debug.lua index af98670c..4feab7ae 100644 --- a/plugins/mod_stanza_debug.lua +++ b/plugins/mod_stanza_debug.lua @@ -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 --git a/plugins/mod_storage_internal.lua b/plugins/mod_storage_internal.lua index fa87e495..a43dd272 100644 --- a/plugins/mod_storage_internal.lua +++ b/plugins/mod_storage_internal.lua @@ -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 @@ function archive:append(username, key, value, when, with) 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 i = 0; + local iter = function() + i = i + 1; + return list[i] end - local count = nil; - local i, last_key = 0; + 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 - end - if not found then - return nil, "item-not-found"; + 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 + 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 @@ function archive:find(username, query) 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 @@ function archive:users() 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 --git a/plugins/mod_storage_memory.lua b/plugins/mod_storage_memory.lua index 9b0024ab..49f94d1d 100644 --- a/plugins/mod_storage_memory.lua +++ b/plugins/mod_storage_memory.lua @@ -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 --git a/plugins/mod_storage_sql.lua b/plugins/mod_storage_sql.lua index b3ed7638..3f606160 100644 --- a/plugins/mod_storage_sql.lua +++ b/plugins/mod_storage_sql.lua @@ -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 is_stanza = require"util.stanza".is_stanza; +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"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 @@ local function deserialize(t, value) 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 @@ local function keyval_store_get() 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 @@ end 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 "<host>", result); + module:log("error", "Unable to read from database %s store for %s: %s", self.store, username or "<host>", 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 @@ end --- 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 @@ function map_store:set_keys(username, keydatas) ("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 @@ function map_store:set_keys(username, keydatas) 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 @@ function archive_store:append(username, key, value, when, with) 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]; + + 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 - 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"; + archive_item_count_cache:set(cache_key, item_count); + else + item_count_cache_hit(); end - archive_item_count_cache:set(cache_key, item_count); - else - item_count_cache_hit(); - end - if archive_item_limit then 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 @@ function archive_store:append(username, key, value, when, with) 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 @@ end 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 @@ local function archive_where(query, args, where) -- 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 @@ function archive_store:delete(username, query) 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 @@ function archive_store:delete(username, query) 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 @@ function archive_store:users() 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 @@ end 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 @@ local function create_table(engine) -- luacheck: ignore 431/engine 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 @@ end 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 @@ local function upgrade_table(engine, params, apply_changes) -- luacheck: ignore 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 @@ end 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 @@ function module.load() 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 @@ function module.command(arg) -- 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 --git a/plugins/mod_storage_xep0227.lua b/plugins/mod_storage_xep0227.lua index 5c3cf7f6..5b324885 100644 --- a/plugins/mod_storage_xep0227.lua +++ b/plugins/mod_storage_xep0227.lua @@ -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 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 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 "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 @@ local handlers = {}; -- 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 --git a/plugins/mod_time.lua b/plugins/mod_time.lua index 0cd5a4ea..4d9e4f4f 100644 --- a/plugins/mod_time.lua +++ b/plugins/mod_time.lua @@ -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 function time_handler(event) 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 --git a/plugins/mod_tls.lua b/plugins/mod_tls.lua index afc1653a..b240a64c 100644 --- a/plugins/mod_tls.lua +++ b/plugins/mod_tls.lua @@ -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 @@ function module.load(reload) 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 @@ end 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 @@ end); -- Hook <starttls/> 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 + -- <proceed/> 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:hook_tag("http://etherx.jabber.org/streams", "features", function (sessio 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 @@ module:hook_tag(xmlns_starttls, "proceed", function (session, stanza) -- luachec 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 --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index c04a1aa4..95b0f8d6 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -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", "map"); +local token_store = module:open_store("auth_tokens", "keyval+"); -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 +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 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_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 token_host ~= module.host then + if grant_host ~= module.host then return nil, "invalid-host"; end - local token_info = { + local grant_id = id.short(); + local now = os.time(); + + local grant = { + id = grant_id; + 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; - - auth_scope = token_scope; - }; + created = now; + expires = grant_ttl and (now + grant_ttl) or nil; + accessed = now; + + jid = grant_jid; + resource = grant_resource; + + data = grant_data; + + -- tokens[<hash-name>..":"..<secret>] = 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_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); + 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 - return token, token_info; + 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) - local token = base64.decode(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_jid, token_id = token:match("^1;([^;]+);(.+)$"); - if not token_jid 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; + return token_id, token_user, token_host, token_secret; end -function get_token_info(token) - local token_id, token_user, token_host = parse_token(token); - if not token_id then - return nil, "invalid-token-format"; +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, err = token_store:get(token_user, token_id); - if not token_info then + 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 + + -- 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 - if token_info.expires and token_info.expires < os.time() then + -- Verify grant validity (expiry, etc.) + grant = _get_validated_grant_info(token_user, grant); + if not grant then return nil, "not-authorized"; end - return 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 -function revoke_token(token) - local token_id, token_user, token_host = parse_token(token); +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, 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 - return token_store:set(token_user, token_id, nil); + 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 + 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 + 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_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 +end) diff --git a/plugins/mod_tombstones.lua b/plugins/mod_tombstones.lua index b5a04c9f..e0f1a827 100644 --- a/plugins/mod_tombstones.lua +++ b/plugins/mod_tombstones.lua @@ -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 --git a/plugins/mod_turn_external.lua b/plugins/mod_turn_external.lua index ee50740c..6cdd8c99 100644 --- a/plugins/mod_turn_external.lua +++ b/plugins/mod_turn_external.lua @@ -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 --git a/plugins/mod_uptime.lua b/plugins/mod_uptime.lua index 8a01fb17..9fbf7612 100644 --- a/plugins/mod_uptime.lua +++ b/plugins/mod_uptime.lua @@ -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 --git a/plugins/mod_user_account_management.lua b/plugins/mod_user_account_management.lua index 130ed089..c2a0e3a2 100644 --- a/plugins/mod_user_account_management.lua +++ b/plugins/mod_user_account_management.lua @@ -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 @@ local function handle_registration_stanza(event) 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 @@ local function handle_registration_stanza(event) return old_session_close(self, ...); end - local ok, err = usermanager_delete_user(username, host); + local old_session = capture_session_properties(session); - 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 + 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(); + }); - log("info", "User removed their account: %s@%s", username, host); - module:fire_event("user-deregistered", { username = username, host = host, source = "mod_register", session = session }); + 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 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 @@ if compat then 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 --git a/plugins/mod_vcard.lua b/plugins/mod_vcard.lua index c3d6fb8b..ea6839bb 100644 --- a/plugins/mod_vcard.lua +++ b/plugins/mod_vcard.lua @@ -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 --git a/plugins/mod_vcard4.lua b/plugins/mod_vcard4.lua index 04dbca9e..1917f609 100644 --- a/plugins/mod_vcard4.lua +++ b/plugins/mod_vcard4.lua @@ -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 --git a/plugins/mod_vcard_legacy.lua b/plugins/mod_vcard_legacy.lua index 107f20da..eb392309 100644 --- a/plugins/mod_vcard_legacy.lua +++ b/plugins/mod_vcard_legacy.lua @@ -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 --git a/plugins/mod_version.lua b/plugins/mod_version.lua index 1d24001c..72b13387 100644 --- a/plugins/mod_version.lua +++ b/plugins/mod_version.lua @@ -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,9 +20,14 @@ if not module:get_option_boolean("hide_os_type") then 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; + local uname, err = pposix.uname(); + if not uname then + module:log("debug", "Could not retrieve OS name: %s", err); + else + platform = uname.sysname; + end end if not platform then local uname = io.popen(os_version_command or "uname"); diff --git a/plugins/mod_watchregistrations.lua b/plugins/mod_watchregistrations.lua index 825b8a73..d433d732 100644 --- a/plugins/mod_watchregistrations.lua +++ b/plugins/mod_watchregistrations.lua @@ -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 --git a/plugins/mod_websocket.lua b/plugins/mod_websocket.lua index f0caa968..7120f3cc 100644 --- a/plugins/mod_websocket.lua +++ b/plugins/mod_websocket.lua @@ -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 parse_close = websocket_frames.parse_close; 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 @@ function module.add_host(module) 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 --git a/plugins/mod_welcome.lua b/plugins/mod_welcome.lua index f6b13df5..0dd0c069 100644 --- a/plugins/mod_welcome.lua +++ b/plugins/mod_welcome.lua @@ -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 --git a/plugins/muc/hats.lib.lua b/plugins/muc/hats.lib.lua index 358e5100..7eb71eb4 100644 --- a/plugins/muc/hats.lib.lua +++ b/plugins/muc/hats.lib.lua @@ -1,7 +1,10 @@ -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"; +local hats_compat = module:get_option_boolean("muc_hats_compat", true); -- COMPAT for pre-XEP namespace, TODO reconsider default for next release + +local xmlns_hats_legacy = "xmpp:prosody.im/protocol/hats:1"; +local xmlns_hats = "urn:xmpp:hats:0"; -- Strip any hats claimed by the client (to prevent spoofing) muc_util.add_filtered_namespace(xmlns_hats); @@ -13,14 +16,26 @@ module:hook("muc-build-occupant-presence", function (event) local hats = aff_data and aff_data.hats; if not hats then return; end local hats_el; + local legacy_hats_el; for hat_id, hat_data in pairs(hats) do if hat_data.active then if not hats_el then hats_el = st.stanza("hats", { xmlns = xmlns_hats }); end hats_el:tag("hat", { uri = hat_id, title = hat_data.title }):up(); + + if hats_compat then + if not legacy_hats_el then + legacy_hats_el = st.stanza("hats", { xmlns = xmlns_hats_legacy }); + end + legacy_hats_el:tag("hat", { uri = hat_id, title = hat_data.title }):up(); + end end end if not hats_el then return; end event.stanza:add_direct_child(hats_el); + + if legacy_hats_el then + event.stanza:add_direct_child(legacy_hats_el); + end end); diff --git a/plugins/muc/hidden.lib.lua b/plugins/muc/hidden.lib.lua index 153df21a..d24fa47e 100644 --- a/plugins/muc/hidden.lib.lua +++ b/plugins/muc/hidden.lib.lua @@ -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 @@ local function set_hidden(room, hidden) 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 @@ module:hook("muc-config-form", function(event) 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 --git a/plugins/muc/history.lib.lua b/plugins/muc/history.lib.lua index 075b1890..005bd1d8 100644 --- a/plugins/muc/history.lib.lua +++ b/plugins/muc/history.lib.lua @@ -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 --git a/plugins/muc/lock.lib.lua b/plugins/muc/lock.lib.lua index 32f2647b..bb5bf82b 100644 --- a/plugins/muc/lock.lib.lua +++ b/plugins/muc/lock.lib.lua @@ -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 --git a/plugins/muc/members_only.lib.lua b/plugins/muc/members_only.lib.lua index b10dc120..4f4e88fa 100644 --- a/plugins/muc/members_only.lib.lua +++ b/plugins/muc/members_only.lib.lua @@ -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 --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index 5873b1a2..1dc99f07 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -86,21 +86,26 @@ room_mt.get_registered_nick = register.get_registered_nick; room_mt.get_registered_jid = register.get_registered_jid; room_mt.handle_register_iq = register.handle_register_iq; +local restrict_pm = module:require "muc/restrict_pm"; +room_mt.get_allow_pm = restrict_pm.get_allow_pm; +room_mt.set_allow_pm = restrict_pm.set_allow_pm; +room_mt.get_allow_modpm = restrict_pm.get_allow_modpm; +room_mt.set_allow_modpm = restrict_pm.set_allow_modpm; + 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 +116,26 @@ module:depends "muc_unique" 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 +if module:get_option_boolean("component_admins_as_room_owners", false) 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 +168,8 @@ local function room_save(room, forced, savestate) 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 +291,16 @@ local function set_room_defaults(room, lang) 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")); + room:set_allow_pm(module:get_option_enum("muc_room_default_allow_pm", room:get_allow_pm(), "visitor", "participant", "moderator")); + room:set_allow_modpm(module:get_option_boolean("muc_room_default_always_allow_moderator_pms", room:get_allow_modpm())); end function create_room(room_jid, config) @@ -350,8 +361,12 @@ function each_room(live_only) end module:hook("host-disco-items", function(event) - local reply = event.reply; 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; 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 +403,7 @@ end); 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 +427,15 @@ if module:get_option_boolean("muc_tombstones", true) then end, -10); end -do - local restrict_room_creation = module:get_option("restrict_room_creation"); - if restrict_room_creation == true then - restrict_room_creation = "admin"; - 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); +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 -end +end); for event_name, method in pairs { -- Normal room interactions @@ -465,7 +469,7 @@ for event_name, method in pairs { 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 +520,10 @@ do -- Ad-hoc commands 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 --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua index 5d8ce868..359afc87 100644 --- a/plugins/muc/muc.lib.lua +++ b/plugins/muc/muc.lib.lua @@ -12,18 +12,18 @@ local pairs = pairs; 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; @@ -1077,7 +1077,10 @@ function room_mt:handle_admin_query_set_command(origin, stanza) 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); @@ -1086,7 +1089,7 @@ function room_mt:handle_admin_query_set_command(origin, stanza) 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 @@ -1117,9 +1120,13 @@ function room_mt:handle_admin_query_get_command(origin, stanza) 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 --git a/plugins/muc/occupant.lib.lua b/plugins/muc/occupant.lib.lua index 8fe4bbdf..a7d9cef7 100644 --- a/plugins/muc/occupant.lib.lua +++ b/plugins/muc/occupant.lib.lua @@ -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 --git a/plugins/muc/occupant_id.lib.lua b/plugins/muc/occupant_id.lib.lua index 1d310b3d..2252799d 100644 --- a/plugins/muc/occupant_id.lib.lua +++ b/plugins/muc/occupant_id.lib.lua @@ -1,12 +1,12 @@ --- Implementation of https://xmpp.org/extensions/inbox/occupant-id.html +-- Implementation of https://xmpp.org/extensions/xep-0421.html -- XEP-0421: Anonymous unique occupant identifiers for MUCs -- (C) 2020 Maxime “pep” Buquet <pep@bouah.net> -- (C) 2020 Matthew Wild <mwild1@gmail.com> -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 --git a/plugins/muc/password.lib.lua b/plugins/muc/password.lib.lua index dd3cb658..9d3c0cca 100644 --- a/plugins/muc/password.lib.lua +++ b/plugins/muc/password.lib.lua @@ -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 --git a/plugins/muc/persistent.lib.lua b/plugins/muc/persistent.lib.lua index c3b16ea4..29ed7784 100644 --- a/plugins/muc/persistent.lib.lua +++ b/plugins/muc/persistent.lib.lua @@ -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 @@ local function set_persistent(room, persistent) 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 @@ module:hook("muc-config-form", function(event) 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 --git a/plugins/muc/presence_broadcast.lib.lua b/plugins/muc/presence_broadcast.lib.lua index 82a89fee..721c47aa 100644 --- a/plugins/muc/presence_broadcast.lib.lua +++ b/plugins/muc/presence_broadcast.lib.lua @@ -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 --git a/plugins/muc/register.lib.lua b/plugins/muc/register.lib.lua index 84045f33..82ccb8ea 100644 --- a/plugins/muc/register.lib.lua +++ b/plugins/muc/register.lib.lua @@ -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 function enforce_nick_policy(event) 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 --git a/plugins/muc/request.lib.lua b/plugins/muc/request.lib.lua index 4e95fdc3..f3786595 100644 --- a/plugins/muc/request.lib.lua +++ b/plugins/muc/request.lib.lua @@ -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 --git a/plugins/muc/restrict_pm.lib.lua b/plugins/muc/restrict_pm.lib.lua new file mode 100644 index 00000000..3c91b921 --- /dev/null +++ b/plugins/muc/restrict_pm.lib.lua @@ -0,0 +1,119 @@ +-- Based on code from mod_muc_restrict_pm in prosody-modules@d82c0383106a +-- by Nicholas George <wirlaburla@worlio.com> + +local st = require "prosody.util.stanza"; +local muc_util = module:require "muc/util"; +local valid_roles = muc_util.valid_roles; + +-- COMPAT w/ prosody-modules allow_pm +local compat_map = { + everyone = "visitor"; + participants = "participant"; + moderators = "moderator"; + members = "affiliated"; +}; + +local function get_allow_pm(room) + local val = room._data.allow_pm; + return compat_map[val] or val or "visitor"; +end + +local function set_allow_pm(room, val) + if get_allow_pm(room) == val then return false; end + room._data.allow_pm = val; + return true; +end + +local function get_allow_modpm(room) + return room._data.allow_modpm or false; +end + +local function set_allow_modpm(room, val) + if get_allow_modpm(room) == val then return false; end + room._data.allow_modpm = val; + return true; +end + +module:hook("muc-config-form", function(event) + local pmval = get_allow_pm(event.room); + table.insert(event.form, { + name = 'muc#roomconfig_allowpm'; + type = 'list-single'; + label = 'Allow private messages from'; + options = { + { value = 'visitor', label = 'Everyone', default = pmval == 'visitor' }; + { value = 'participant', label = 'Participants', default = pmval == 'participant' }; + { value = 'moderator', label = 'Moderators', default = pmval == 'moderator' }; + { value = 'affiliated', label = "Members", default = pmval == "affiliated" }; + { value = 'none', label = 'No one', default = pmval == 'none' }; + } + }); + table.insert(event.form, { + name = '{xmpp:prosody.im}muc#allow_modpm'; + type = 'boolean'; + label = 'Always allow private messages to moderators'; + value = get_allow_modpm(event.room) + }); +end); + +module:hook("muc-config-submitted/muc#roomconfig_allowpm", function(event) + if set_allow_pm(event.room, event.value) then + event.status_codes["104"] = true; + end +end); + +module:hook("muc-config-submitted/{xmpp:prosody.im}muc#allow_modpm", function(event) + if set_allow_modpm(event.room, event.value) then + event.status_codes["104"] = true; + end +end); + +local who_restricted = { + none = "in this group"; + participant = "from guests"; + moderator = "from non-moderators"; + affiliated = "from non-members"; +}; + +module:hook("muc-private-message", function(event) + local stanza, room = event.stanza, event.room; + local from_occupant = room:get_occupant_by_nick(stanza.attr.from); + local to_occupant = room:get_occupant_by_nick(stanza.attr.to); + + -- To self is always okay + if to_occupant.bare_jid == from_occupant.bare_jid then return; end + + if get_allow_modpm(room) then + if to_occupant and to_occupant.role == 'moderator' + or from_occupant and from_occupant.role == "moderator" then + return; -- Allow to/from moderators + end + end + + local pmval = get_allow_pm(room); + + if pmval ~= "none" then + if pmval == "affiliated" and room:get_affiliation(from_occupant.bare_jid) then + return; -- Allow from affiliated users + elseif valid_roles[from_occupant.role] >= valid_roles[pmval] then + module:log("debug", "Allowing PM: %s(%d) >= %s(%d)", from_occupant.role, valid_roles[from_occupant.role], pmval, valid_roles[pmval]); + return; -- Allow from a permitted role + end + end + + local msg = ("Private messages are restricted %s"):format(who_restricted[pmval]); + module:log("debug", "Blocking PM from %s %s: %s", from_occupant.role, stanza.attr.from, msg); + + room:route_to_occupant( + from_occupant, + st.error_reply(stanza, "cancel", "policy-violation", msg, room.jid) + ); + return false; +end, 1); + +return { + get_allow_pm = get_allow_pm; + set_allow_pm = set_allow_pm; + get_allow_modpm = get_allow_modpm; + set_allow_modpm = set_allow_modpm; +}; diff --git a/plugins/muc/subject.lib.lua b/plugins/muc/subject.lib.lua index 3230817c..047ea2df 100644 --- a/plugins/muc/subject.lib.lua +++ b/plugins/muc/subject.lib.lua @@ -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; @@ -44,9 +44,19 @@ if CFG_DATADIR then 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 + +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end -local startup = require "util.startup"; -local async = require "util.async"; +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 @@ local function loop() 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); @@ -44,22 +44,33 @@ end ----------- -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 read_password = human_io.read_password; 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 @@ function commands.about(arg) 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 @@ function commands.about(arg) .."\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 @@ function commands.about(arg) 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 @@ function commands.about(arg) } 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 @@ function commands.about(arg) 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 @@ function commands.reload(arg) 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 @@ function commands.reload(arg) 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 @@ 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); @@ -675,7 +695,7 @@ local command_runner = async.runner(function () 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 @@ local command_runner = async.runner(function () 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 --git a/spec/core_moduleapi_spec.lua b/spec/core_moduleapi_spec.lua index 20431935..21261385 100644 --- a/spec/core_moduleapi_spec.lua +++ b/spec/core_moduleapi_spec.lua @@ -11,8 +11,8 @@ local api = require "core.moduleapi"; 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 @@ function module:get_option(name) 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 --git a/spec/core_storagemanager_spec.lua b/spec/core_storagemanager_spec.lua index ae4f44c8..f1adcd50 100644 --- a/spec/core_storagemanager_spec.lua +++ b/spec/core_storagemanager_spec.lua @@ -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 configs = { }; }; +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 @@ describe("storagemanager", function () 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 @@ describe("storagemanager", function () 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 @@ describe("storagemanager", function () 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 @@ describe("storagemanager", function () 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 @@ describe("storagemanager", function () 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 @@ describe("storagemanager", function () 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 @@ describe("storagemanager", function () 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 @@ describe("storagemanager", function () 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 --git a/spec/inputs/test_keys.lua b/spec/inputs/test_keys.lua new file mode 100644 index 00000000..e0e9ff8c --- /dev/null +++ b/spec/inputs/test_keys.lua @@ -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 --git a/spec/net_resolvers_service_spec.lua b/spec/net_resolvers_service_spec.lua new file mode 100644 index 00000000..53ce4754 --- /dev/null +++ b/spec/net_resolvers_service_spec.lua @@ -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 --git a/spec/scansion/bookmarks2.scs b/spec/scansion/bookmarks2.scs new file mode 100644 index 00000000..0243ca54 --- /dev/null +++ b/spec/scansion/bookmarks2.scs @@ -0,0 +1,181 @@ +# Pubsub: Bookmarks 2.0 + +[Client] Juliet + jid: admin@localhost + password: password + +// admin@localhost is assumed to have node creation privileges + +--------- + +Juliet connects + +-- Generated with https://gitlab.com/xmpp-rs/xmpp-parsers: +-- cargo run --example=generate-caps https://code.matthewwild.co.uk/scansion/ <<< "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' name='scansion' type='bot'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='urn:xmpp:bookmarks:1+notify'/></query>" +Juliet sends: + <presence id='presence0'> + <c xmlns='http://jabber.org/protocol/caps' + hash='sha-1' + node='https://code.matthewwild.co.uk/scansion/' + ver='CPuQARM1gCTq2f6/ZjHUzWL2QHg='/> + <c xmlns='urn:xmpp:caps'> + <hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>OTy9GPCvBZRvqzOHmD/ThA1WbBH3tNoeKbdqKQCRPHc=</hash> + <hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>f/rxDeTf6HyjQ382V3GEG/UfAs5IeclC05jBSBnVQCI=</hash> + <hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>ucfqg/NrLj0omE+26hYMrbpcmxHcU4Z3hfAQIF+6tt0=</hash> + </c> + </presence> + +Juliet receives: + <iq from="${Juliet's JID}" id='disco' type='get'> + <query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='/> + </iq> + +Juliet sends: + <iq to="${Juliet's JID}" id='disco' type='result'> + <query xmlns='http://jabber.org/protocol/disco#info' node='https://code.matthewwild.co.uk/scansion/#CPuQARM1gCTq2f6/ZjHUzWL2QHg='> + <identity category='client' name='scansion' type='bot'/> + <feature var='http://jabber.org/protocol/disco#info'/> + <feature var='urn:xmpp:bookmarks:1+notify'/> + </query> + </iq> + +Juliet sends: + <iq type='set' id='pub0'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='theplay@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Play's the Thing' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </publish> + <publish-options> + <x xmlns='jabber:x:data' type='submit'> + <field var='FORM_TYPE' type='hidden'> + <value>http://jabber.org/protocol/pubsub#publish-options</value> + </field> + <field var='pubsub#persist_items'> + <value>true</value> + </field> + <field var='pubsub#max_items'> + <value>255</value> + </field> + <field var='pubsub#send_last_published_item'> + <value>never</value> + </field> + <field var='pubsub#access_model'> + <value>whitelist</value> + </field> + </x> + </publish-options> + </pubsub> + </iq> + +Juliet receives: + <message type='headline' from="${Juliet's JID}"> + <event xmlns='http://jabber.org/protocol/pubsub#event'> + <items node='urn:xmpp:bookmarks:1'> + <item id='theplay@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Play's the Thing' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </items> + </event> + </message> + +Juliet receives: + <iq type='result' id='pub0'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='theplay@conference.shakespeare.lit'/> + </publish> + </pubsub> + </iq> + +Juliet sends: + <iq type='set' id='pub1'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='orchard@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Orchard' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </publish> + <publish-options> + <x xmlns='jabber:x:data' type='submit'> + <field var='FORM_TYPE' type='hidden'> + <value>http://jabber.org/protocol/pubsub#publish-options</value> + </field> + <field var='pubsub#persist_items'> + <value>true</value> + </field> + <field var='pubsub#max_items'> + <value>255</value> + </field> + <field var='pubsub#send_last_published_item'> + <value>never</value> + </field> + <field var='pubsub#access_model'> + <value>whitelist</value> + </field> + </x> + </publish-options> + </pubsub> + </iq> + +Juliet receives: + <message type='headline' from="${Juliet's JID}"> + <event xmlns='http://jabber.org/protocol/pubsub#event'> + <items node='urn:xmpp:bookmarks:1'> + <item id='orchard@conference.shakespeare.lit'> + <conference xmlns='urn:xmpp:bookmarks:1' + name='The Orchard' + autojoin='true'> + <nick>JC</nick> + </conference> + </item> + </items> + </event> + </message> + +Juliet receives: + <iq type='result' id='pub1'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <publish node='urn:xmpp:bookmarks:1'> + <item id='orchard@conference.shakespeare.lit'/> + </publish> + </pubsub> + </iq> + +Juliet sends: + <iq type='set' id='retract0'> + <pubsub xmlns='http://jabber.org/protocol/pubsub'> + <retract node='urn:xmpp:bookmarks:1' notify='1'> + <item id='theplay@conference.shakespeare.lit'/> + </retract> + </pubsub> + </iq> + +Juliet receives: + <message type='headline' from="${Juliet's JID}"> + <event xmlns='http://jabber.org/protocol/pubsub#event'> + <items node='urn:xmpp:bookmarks:1'> + <retract id='theplay@conference.shakespeare.lit'/> + </items> + </event> + </message> + +Juliet receives: + <iq type='result' id='retract0'/> + +Juliet disconnects + +// vim: syntax=xml: diff --git a/spec/scansion/extdisco.scs b/spec/scansion/extdisco.scs index f0781dc5..f2c8139f 100644 --- a/spec/scansion/extdisco.scs +++ b/spec/scansion/extdisco.scs @@ -17,8 +17,8 @@ Romeo receives: <iq type='result' id='lx2' from='localhost'> <services xmlns='urn:xmpp:extdisco:2'> <service host='default.example' transport='udp' port='9876' type='stun'/> - <service port='9876' type='turn' restricted='1' password='yHYYBDN7M3mdlug0LTdJbW0GvvQ=' transport='udp' host='default.example' username='1219525744'/> - <service port='9876' type='turn' restricted='1' password='1Uc6QfrDhIlbK97rGCUQ/cUICxs=' transport='udp' host='default.example' username='1219525744'/> + <service port='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/> + <service port='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/> <service port='2121' type='ftp' restricted='1' password='password' transport='tcp' host='default.example' username='john'/> <service port='21' type='ftp' restricted='1' password='password' transport='tcp' host='ftp.example.com' username='john'/> </services> @@ -47,8 +47,8 @@ Romeo sends: Romeo receives: <iq type='result' id='lx4' from='localhost'> <credentials xmlns='urn:xmpp:extdisco:2'> - <service port='9876' type='turn' restricted='1' password='yHYYBDN7M3mdlug0LTdJbW0GvvQ=' transport='udp' host='default.example' username='1219525744'/> - <service port='9876' type='turn' restricted='1' password='1Uc6QfrDhIlbK97rGCUQ/cUICxs=' transport='udp' host='default.example' username='1219525744'/> + <service port='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/> + <service port='9876' type='turn' restricted='1' password='{scansion:any}' transport='udp' host='default.example' username='{scansion:any}'/> </credentials> </iq> diff --git a/spec/scansion/lastactivity.scs b/spec/scansion/lastactivity.scs index 44f4e516..4b8c8573 100644 --- a/spec/scansion/lastactivity.scs +++ b/spec/scansion/lastactivity.scs @@ -37,7 +37,7 @@ Romeo sends: Romeo receives: <iq type='result' id='a'> - <query xmlns='jabber:iq:last' seconds='0'>Goodbye</query> + <query xmlns='jabber:iq:last' seconds='{scansion:any}'>Goodbye</query> </iq> Romeo disconnects diff --git a/spec/scansion/mam_extended.scs b/spec/scansion/mam_extended.scs index 2c6840df..e79eba95 100644 --- a/spec/scansion/mam_extended.scs +++ b/spec/scansion/mam_extended.scs @@ -45,8 +45,8 @@ Romeo sends: Romeo receives: <iq type="result" id="mamextmeta"> <metadata xmlns="urn:xmpp:mam:2"> - <start timestamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:mam:2" id="{scansion:any}"/> - <end timestamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:mam:2" id="{scansion:any}"/> + <start timestamp="{scansion:capture:start}" xmlns="urn:xmpp:mam:2" id="{scansion:capture:first}"/> + <end timestamp="{scansion:capture:end}" xmlns="urn:xmpp:mam:2" id="{scansion:capture:last}"/> </metadata> </iq> @@ -57,9 +57,9 @@ Romeo sends: Romeo receives: <message to="${Romeo's full JID}"> - <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}"> + <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:capture:first}"> <forwarded xmlns="urn:xmpp:forward:0"> - <delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/> + <delay stamp="{scansion:capture:start}" xmlns="urn:xmpp:delay"/> <message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat01" from="${Romeo's full JID}"> <body>Hello</body> </message> @@ -69,9 +69,9 @@ Romeo receives: Romeo receives: <message to="${Romeo's full JID}"> - <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}"> + <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:capture:last}"> <forwarded xmlns="urn:xmpp:forward:0"> - <delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/> + <delay stamp="{scansion:capture:end}" xmlns="urn:xmpp:delay"/> <message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat02" from="${Romeo's full JID}"> <body>U there?</body> </message> @@ -98,7 +98,7 @@ Romeo receives: <message to="${Romeo's full JID}"> <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}"> <forwarded xmlns="urn:xmpp:forward:0"> - <delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/> + <delay stamp="{scansion:capture:start}" xmlns="urn:xmpp:delay"/> <message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat02" from="${Romeo's full JID}"> <body>U there?</body> </message> @@ -110,7 +110,7 @@ Romeo receives: <message to="${Romeo's full JID}"> <result xmlns="urn:xmpp:mam:2" queryid="q1" id="{scansion:any}"> <forwarded xmlns="urn:xmpp:forward:0"> - <delay stamp="2008-08-22T21:09:04Z" xmlns="urn:xmpp:delay"/> + <delay stamp="{scansion:capture:end}" xmlns="urn:xmpp:delay"/> <message to="someone@localhost" xmlns="jabber:client" type="chat" xml:lang="en" id="chat01" from="${Romeo's full JID}"> <body>Hello</body> </message> diff --git a/spec/scansion/muc_outcast_reason.scs b/spec/scansion/muc_outcast_reason.scs new file mode 100644 index 00000000..e2725653 --- /dev/null +++ b/spec/scansion/muc_outcast_reason.scs @@ -0,0 +1,72 @@ +# Save ban reason + +[Client] Romeo + password: password + jid: user@localhost + +----- + +Romeo connects + +Romeo sends: + <presence to="muc-outcast-reason@conference.localhost/Romeo"> + <x xmlns="http://jabber.org/protocol/muc"/> + </presence> + +Romeo receives: + <presence from="muc-outcast-reason@conference.localhost/Romeo"> + <x xmlns="http://jabber.org/protocol/muc#user"> + <status code="201"/> + <item jid="${Romeo's full JID}" role="moderator" affiliation="owner"/> + <status code="110"/> + </x> + </presence> + +Romeo receives: + <message type="groupchat" from="muc-outcast-reason@conference.localhost"> + <subject/> + </message> + +Romeo sends: + <iq id="lx5" to="muc-outcast-reason@conference.localhost" type="set"> + <query xmlns="http://jabber.org/protocol/muc#admin"> + <item affiliation="outcast" jid="tybalt@localhost"> + <reason>Hey calm down</reason> + </item> + </query> + </iq> + +Romeo receives: + <message from="muc-outcast-reason@conference.localhost"> + <x xmlns="http://jabber.org/protocol/muc#user"> + <status code="301"/> + <item jid="tybalt@localhost" affiliation="outcast"> + <reason>Hey calm down</reason> + </item> + </x> + </message> + +Romeo receives: + <iq id="lx5" type="result" from="muc-outcast-reason@conference.localhost"/> + +Romeo sends: + <iq id="lx6" to="muc-outcast-reason@conference.localhost" type="get"> + <query xmlns="http://jabber.org/protocol/muc#admin"> + <item affiliation="outcast"/> + </query> + </iq> + +Romeo receives: + <iq id="lx6" type="result" from="muc-outcast-reason@conference.localhost"> + <query xmlns="http://jabber.org/protocol/muc#admin"> + <item jid="tybalt@localhost" affiliation="outcast"> + <reason>Hey calm down</reason> + </item> + </query> + </iq> + +Romeo disconnects + +Romeo sends: + <presence type='unavailable'/> + diff --git a/spec/scansion/muc_subject_issue_667.scs b/spec/scansion/muc_subject_issue_667.scs index 74980073..a4544ce4 100644 --- a/spec/scansion/muc_subject_issue_667.scs +++ b/spec/scansion/muc_subject_issue_667.scs @@ -42,6 +42,21 @@ Romeo receives: <body>Hello everyone</body> </message> +# this should be treated as a normal message +Romeo sends: + <message to="issue667@conference.localhost" type="groupchat"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + +Romeo receives: + <message type="groupchat" from="issue667@conference.localhost/Romeo"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + # Resync Romeo sends: <presence to="issue667@conference.localhost/Romeo"> @@ -63,6 +78,13 @@ Romeo receives: <body>Hello everyone</body> </message> +Romeo receives: + <message type="groupchat" from="issue667@conference.localhost/Romeo"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + # the still empty subject Romeo receives: <message type="groupchat" from="issue667@conference.localhost"> @@ -116,6 +138,13 @@ Romeo receives: Romeo receives: <message type="groupchat" from="issue667@conference.localhost/Romeo"> + <subject>New thread</subject> + <thread>498acea5-5894-473f-b4c6-c77319d11c75</thread> + <store xmlns="urn:xmpp:hints"/> + </message> + +Romeo receives: + <message type="groupchat" from="issue667@conference.localhost/Romeo"> <body>Lorem ipsum dolor sit amet</body> </message> diff --git a/spec/scansion/pep_itemreply.scs b/spec/scansion/pep_itemreply.scs new file mode 100644 index 00000000..878b9e99 --- /dev/null +++ b/spec/scansion/pep_itemreply.scs @@ -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: + <presence> + <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/> + </presence> + +Romeo receives: + <iq type='get' id='disco' from="${Romeo's JID}"> + <query node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY=' xmlns='http://jabber.org/protocol/disco#info'/> + </iq> + +Romeo receives: + <presence from="${Romeo's full JID}"> + <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/> + </presence> + +Romeo sends: + <iq type='get' id='6'> + <query ver='' xmlns='jabber:iq:roster'/> + </iq> + +Romeo receives: + <iq type='result' id='6'> + <query ver='1' xmlns='jabber:iq:roster'/> + </iq> + +Juliet connects + +Juliet sends: + <presence> + <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/> + </presence> + +Juliet receives: + <iq type='get' id='disco' from="${Juliet's JID}"> + <query node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY=' xmlns='http://jabber.org/protocol/disco#info'/> + </iq> + +Juliet receives: + <presence from="${Juliet's full JID}"> + <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/> + </presence> + +Juliet sends: + <iq type='get' id='6'> + <query ver='' xmlns='jabber:iq:roster'/> + </iq> + +Juliet receives: + <iq type='result' id='6'> + <query ver='1' xmlns='jabber:iq:roster'/> + </iq> + +Romeo sends: + <iq type='result' id='disco' to='pep-test-df6zdvkv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/></query></iq> + +Romeo sends: + <presence type='subscribe' to="${Juliet's JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo receives: + <iq type='set' id='{scansion:any}'><query ver='1' xmlns='jabber:iq:roster'><item ask='subscribe' jid='pep-test-5k90xvps@localhost' subscription='none'/></query></iq> + +Romeo receives: + <presence type='unavailable' to='pep-test-df6zdvkv@localhost' from='pep-test-5k90xvps@localhost'/> + +Juliet receives: + <presence type='subscribe' from='pep-test-df6zdvkv@localhost' to='pep-test-5k90xvps@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Juliet sends: + <iq type='result' id='disco' to='pep-test-5k90xvps@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#PDH7CGVPRERS2WUqBD18PHGEzaY='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/></query></iq> + +Juliet sends: + <presence type='subscribe' to="${Romeo's JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Juliet receives: + <iq type='set' id='{scansion:any}'><query ver='2' xmlns='jabber:iq:roster'><item ask='subscribe' jid='pep-test-df6zdvkv@localhost' subscription='none'/></query></iq> + +Juliet receives: + <presence type='unavailable' to='pep-test-5k90xvps@localhost' from='pep-test-df6zdvkv@localhost'/> + +Romeo receives: + <presence type='subscribe' from='pep-test-5k90xvps@localhost' to='pep-test-df6zdvkv@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo sends: + <iq type='result' id='fixme'/> + +Romeo sends: + <presence type='subscribed' to='pep-test-5k90xvps@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo receives: + <iq type='set' id='{scansion:any}'><query ver='3' xmlns='jabber:iq:roster'><item ask='subscribe' jid='pep-test-5k90xvps@localhost' subscription='from'/></query></iq> + +Juliet receives: + <presence type='subscribed' from='pep-test-df6zdvkv@localhost' to='pep-test-5k90xvps@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Juliet receives: + <iq type='set' id='{scansion:any}'><query ver='3' xmlns='jabber:iq:roster'><item jid='pep-test-df6zdvkv@localhost' subscription='to'/></query></iq> + +Juliet receives: + <presence to='pep-test-5k90xvps@localhost' from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/><delay xmlns='urn:xmpp:delay' stamp='{scansion:any}' from='localhost'/></presence> + +Juliet sends: + <presence type='subscribed' to='pep-test-df6zdvkv@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Juliet receives: + <iq type='set' id='{scansion:any}'><query ver='4' xmlns='jabber:iq:roster'><item jid='pep-test-df6zdvkv@localhost' subscription='both'/></query></iq> + +Juliet receives: + <presence to='pep-test-5k90xvps@localhost' from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/><delay xmlns='urn:xmpp:delay' stamp='{scansion:any}' from='localhost'/></presence> + +Romeo receives: + <presence type='subscribed' from='pep-test-5k90xvps@localhost' to='pep-test-df6zdvkv@localhost'><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo receives: + <iq type='set' id='{scansion:any}'><query ver='4' xmlns='jabber:iq:roster'><item jid='pep-test-5k90xvps@localhost' subscription='both'/></query></iq> + +Romeo receives: + <presence to='pep-test-df6zdvkv@localhost' from="${Juliet's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='PDH7CGVPRERS2WUqBD18PHGEzaY=' node='http://code.matthewwild.co.uk/verse/'/><delay xmlns='urn:xmpp:delay' stamp='{scansion:any}' from='localhost'/></presence> + +Juliet sends: + <iq type='result' id='fixme'/> + +Romeo sends: + <iq type='result' id='fixme'/> + +Romeo sends: + <iq type='result' id='fixme'/> + +Romeo sends: + <presence><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='m/sIsyfzKk8X1okZMtStR43nQQg=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo receives: + <iq type='get' id='disco' from='pep-test-df6zdvkv@localhost'><query node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg=' xmlns='http://jabber.org/protocol/disco#info'/></iq> + +Romeo receives: + <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='m/sIsyfzKk8X1okZMtStR43nQQg=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo receives: + <iq type='get' id='disco' from='pep-test-5k90xvps@localhost'><query node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg=' xmlns='http://jabber.org/protocol/disco#info'/></iq> + +Juliet receives: + <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='m/sIsyfzKk8X1okZMtStR43nQQg=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo sends: + <presence><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='IfQwbaaDB4LEP5tkGArEaB/3Y+s=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo receives: + <iq type='get' id='disco' from='pep-test-df6zdvkv@localhost'><query node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s=' xmlns='http://jabber.org/protocol/disco#info'/></iq> + +Romeo receives: + <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='IfQwbaaDB4LEP5tkGArEaB/3Y+s=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Romeo receives: + <iq type='get' id='disco' from='pep-test-5k90xvps@localhost'><query node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s=' xmlns='http://jabber.org/protocol/disco#info'/></iq> + +Romeo sends: + <iq type='result' id='disco' to='pep-test-df6zdvkv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg='/></iq> + +Romeo sends: + <iq type='result' id='disco' to='pep-test-5k90xvps@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#m/sIsyfzKk8X1okZMtStR43nQQg='/></iq> + +Romeo sends: + <iq type='result' id='disco' to='pep-test-df6zdvkv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/tune+notify'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/><feature var='http://jabber.org/protocol/mood+notify'/></query></iq> + +Juliet receives: + <presence from="${Romeo's full JID}"><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' ver='IfQwbaaDB4LEP5tkGArEaB/3Y+s=' node='http://code.matthewwild.co.uk/verse/'/></presence> + +Juliet sends: + <iq type='result' id='fixme'/> + +Juliet sends: + <iq type='set' id='7'><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></publish><publish-options><x type='submit' xmlns='jabber:x:data'><field type='hidden' var='FORM_TYPE'><value>http://jabber.org/protocol/pubsub#publish-options</value></field><field var='pubsub#persist_items'><value>true</value></field><field var='pubsub#itemreply'><value>publisher</value></field></x></publish-options></pubsub></iq> + +Juliet receives: + <iq type='result' id='7' ><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current'/></publish></pubsub></iq> + +Romeo receives: + <message type='headline' from='pep-test-5k90xvps@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message> + +Romeo sends: + <iq type='result' id='disco' to='pep-test-5k90xvps@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/tune+notify'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/><feature var='http://jabber.org/protocol/mood+notify'/></query></iq> + +Romeo receives: + <message type='headline' from='pep-test-5k90xvps@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message> + +Juliet disconnects + +Romeo disconnects diff --git a/spec/scansion/pep_nickname.scs b/spec/scansion/pep_nickname.scs index aaf53c87..1e39415b 100644 --- a/spec/scansion/pep_nickname.scs +++ b/spec/scansion/pep_nickname.scs @@ -58,7 +58,7 @@ Romeo receives: <message type="headline" from="romeo@localhost"> <event xmlns="http://jabber.org/protocol/pubsub#event"> <items node="http://jabber.org/protocol/nick"> - <item id="current" publisher="${Romeo's JID}"> + <item id="current"> <nickname xmlns="http://jabber.org/protocol/nick"/> </item> </items> diff --git a/spec/scansion/pep_publish_subscribe.scs b/spec/scansion/pep_publish_subscribe.scs index 6d33ffeb..e8080134 100644 --- a/spec/scansion/pep_publish_subscribe.scs +++ b/spec/scansion/pep_publish_subscribe.scs @@ -182,7 +182,7 @@ Juliet sends: <iq type='result' id='fixme'/> Juliet sends: - <iq type='set' id='7'><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></publish></pubsub></iq> + <iq type='set' id='7'><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current'><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></publish></pubsub></iq> Juliet receives: <iq type='result' id='7' ><pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='http://jabber.org/protocol/tune'><item id='current'/></publish></pubsub></iq> @@ -197,13 +197,13 @@ Juliet sends: <iq type='result' id='{scansion:any}'/> Romeo receives: - <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message> + <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current'><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message> Romeo sends: <iq type='result' id='disco' to='pep-test-tqvqu_pv@localhost'><query xmlns='http://jabber.org/protocol/disco#info' node='http://code.matthewwild.co.uk/verse/#IfQwbaaDB4LEP5tkGArEaB/3Y+s='><identity type='pc' name='Verse' category='client'/><feature var='http://jabber.org/protocol/tune+notify'/><feature var='http://jabber.org/protocol/disco#info'/><feature var='http://jabber.org/protocol/disco#items'/><feature var='http://jabber.org/protocol/caps'/><feature var='http://jabber.org/protocol/mood+notify'/></query></iq> Romeo receives: - <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current' publisher="${Juliet's JID}"><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message> + <message type='headline' from='pep-test-tqvqu_pv@localhost'><event xmlns='http://jabber.org/protocol/pubsub#event'><items node='http://jabber.org/protocol/tune'><item id='current'><tune xmlns='http://jabber.org/protocol/tune'><title>Beautiful Cedars</title><artist>The Spinners</artist><source>Not Quite Folk</source><track>4</track></tune></item></items></event></message> Juliet disconnects diff --git a/spec/scansion/prosody.cfg.lua b/spec/scansion/prosody.cfg.lua index 75f55ae0..58889fd7 100644 --- a/spec/scansion/prosody.cfg.lua +++ b/spec/scansion/prosody.cfg.lua @@ -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 @@ modules_enabled = { "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 --git a/spec/scansion/pubsub_config.scs b/spec/scansion/pubsub_config.scs index d06d864e..a2b2ab68 100644 --- a/spec/scansion/pubsub_config.scs +++ b/spec/scansion/pubsub_config.scs @@ -74,6 +74,7 @@ Romeo receives: </option> <value>presence</value> </field> + <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/> <field var="pubsub#publish_model" label="Specify the publisher model" type="list-single"> <option label="publishers"> <value>publishers</value> @@ -119,6 +120,18 @@ Romeo receives: <field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean"> <value>1</value> </field> + <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single"> + <option label="Include the node owner's JID"> + <value>owner</value> + </option> + <option label="Include the item publisher's JID"> + <value>publisher</value> + </option> + <option label="Don't include any JID with items"> + <value>none</value> + </option> + <value>none</value> + </field> </x> </configure> </pubsub> @@ -164,6 +177,7 @@ Romeo sends: </option> <value>presence</value> </field> + <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/> <field var="pubsub#publish_model" type="list-single" label="Specify the publisher model"> <option label="publishers"> <value>publishers</value> diff --git a/spec/scansion/pubsub_max_items.scs b/spec/scansion/pubsub_max_items.scs index c5525bd3..b0ead5bf 100644 --- a/spec/scansion/pubsub_max_items.scs +++ b/spec/scansion/pubsub_max_items.scs @@ -69,6 +69,7 @@ Alice receives: </option> <value>open</value> </field> + <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/> <field var="pubsub#publish_model" label="Specify the publisher model" type="list-single"> <option label="publishers"> <value>publishers</value> @@ -114,6 +115,18 @@ Alice receives: <field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean"> <value>1</value> </field> + <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single"> + <option label="Include the node owner's JID"> + <value>owner</value> + </option> + <option label="Include the item publisher's JID"> + <value>publisher</value> + </option> + <option label="Don't include any JID with items"> + <value>none</value> + </option> + <value>none</value> + </field> </x> </configure> </pubsub> diff --git a/spec/scansion/pubsub_multi_items.scs b/spec/scansion/pubsub_multi_items.scs index e43bc839..8c6872e4 100644 --- a/spec/scansion/pubsub_multi_items.scs +++ b/spec/scansion/pubsub_multi_items.scs @@ -69,6 +69,7 @@ Alice receives: </option> <value>open</value> </field> + <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/> <field var="pubsub#publish_model" label="Specify the publisher model" type="list-single"> <option label="publishers"> <value>publishers</value> @@ -114,6 +115,18 @@ Alice receives: <field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean"> <value>1</value> </field> + <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single"> + <option label="Include the node owner's JID"> + <value>owner</value> + </option> + <option label="Include the item publisher's JID"> + <value>publisher</value> + </option> + <option label="Don't include any JID with items"> + <value>none</value> + </option> + <value>none</value> + </field> </x> </configure> </pubsub> diff --git a/spec/scansion/pubsub_preconditions.scs b/spec/scansion/pubsub_preconditions.scs index 5c0c2569..fe056fc6 100644 --- a/spec/scansion/pubsub_preconditions.scs +++ b/spec/scansion/pubsub_preconditions.scs @@ -73,6 +73,7 @@ Romeo receives: </option> <value>presence</value> </field> + <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/> <field var="pubsub#publish_model" label="Specify the publisher model" type="list-single"> <option label="publishers"> <value>publishers</value> @@ -118,6 +119,18 @@ Romeo receives: <field var="pubsub#notify_retract" label="Whether to notify subscribers when items are removed from the node" type="boolean"> <value>1</value> </field> + <field label="Specify whose JID to include as the publisher of items" var="pubsub#itemreply" type="list-single"> + <option label="Include the node owner's JID"> + <value>owner</value> + </option> + <option label="Include the item publisher's JID"> + <value>publisher</value> + </option> + <option label="Don't include any JID with items"> + <value>none</value> + </option> + <value>none</value> + </field> </x> </configure> </pubsub> @@ -163,6 +176,7 @@ Romeo sends: </option> <value>presence</value> </field> + <field type="list-multi" var="pubsub#roster_groups_allowed" label="Roster groups allowed to subscribe"/> <field var="pubsub#publish_model" type="list-single" label="Specify the publisher model"> <option label="publishers"> <value>publishers</value> @@ -199,6 +213,9 @@ Romeo sends: <field var="pubsub#notify_retract" type="boolean" label="Whether to notify subscribers when items are removed from the node"> <value>1</value> </field> + <field var="pubsub#itemreply" type="boolean"> + <value>none</value> + </field> </x> </configure> </pubsub> diff --git a/spec/scansion/uptime.scs b/spec/scansion/uptime.scs index 188b9eb5..ed83e51b 100644 --- a/spec/scansion/uptime.scs +++ b/spec/scansion/uptime.scs @@ -15,7 +15,7 @@ Romeo sends: Romeo receives: <iq type='result' id='a' from='localhost'> - <query xmlns='jabber:iq:last' seconds='0'/> + <query xmlns='jabber:iq:last' seconds='{scansion:any}'/> </iq> Romeo disconnects diff --git a/spec/scansion/vcard_temp.scs b/spec/scansion/vcard_temp.scs index 38c6f755..240ea1f3 100644 --- a/spec/scansion/vcard_temp.scs +++ b/spec/scansion/vcard_temp.scs @@ -51,8 +51,6 @@ Romeo receives: </vCard> </iq> -Romeo disconnects - Juliet connects Juliet sends: @@ -77,4 +75,6 @@ Juliet receives: Juliet disconnects +Romeo disconnects + # recording ended on 2018-10-20T15:02:14Z diff --git a/spec/util_argparse_spec.lua b/spec/util_argparse_spec.lua index 0f2430b7..40f647c9 100644 --- a/spec/util_argparse_spec.lua +++ b/spec/util_argparse_spec.lua @@ -50,4 +50,9 @@ describe("parse", function() 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 --git a/spec/util_bitcompat_spec.lua b/spec/util_bitcompat_spec.lua index 34a87f5b..99642821 100644 --- a/spec/util_bitcompat_spec.lua +++ b/spec/util_bitcompat_spec.lua @@ -24,4 +24,8 @@ describe("util.bitcompat", function () 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 --git a/spec/util_cache_spec.lua b/spec/util_cache_spec.lua index d57e25ac..ae7b1936 100644 --- a/spec/util_cache_spec.lua +++ b/spec/util_cache_spec.lua @@ -4,6 +4,20 @@ local cache = require "util.cache"; 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 @@ describe("util.cache", function() 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 @@ describe("util.cache", function() 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 --git a/spec/util_crypto_spec.lua b/spec/util_crypto_spec.lua new file mode 100644 index 00000000..4a62e0bc --- /dev/null +++ b/spec/util_crypto_spec.lua @@ -0,0 +1,205 @@ +local test_keys = require "spec.inputs.test_keys"; + +describe("util.crypto", function () + local crypto = require "util.crypto"; + local random = require "util.random"; + local encodings = require "util.encodings"; + + describe("generate_ed25519_keypair", function () + local keypair = crypto.generate_ed25519_keypair(); + assert.is_not_nil(keypair); + assert.equal("ED25519", keypair:get_type()); + end) + + describe("generate_p256_keypair", function () + local keypair = crypto.generate_p256_keypair(); + assert.is_not_nil(keypair); + assert.equal("id-ecPublicKey", keypair:get_type()); + end) + + describe("export/import raw", function () + local keypair = crypto.generate_p256_keypair(); + assert.is_not_nil(keypair); + local raw = keypair:public_raw() + local imported = crypto.import_public_ec_raw(raw, "P-256") + assert.equal(keypair:public_pem(), imported:public_pem()); + end) + + describe("derive", function () + local key = crypto.import_private_pem(test_keys.ecdsa_private_pem); + local peer_key = crypto.import_public_pem(test_keys.ecdsa_public_pem); + assert.equal("n1v4KeKmOVwjC67fiKtjJnqcEaasbpZa2fLPNHW51co=", encodings.base64.encode(key:derive(peer_key))) + 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 --git a/spec/util_dataforms_spec.lua b/spec/util_dataforms_spec.lua index 5293238a..ab402fdb 100644 --- a/spec/util_dataforms_spec.lua +++ b/spec/util_dataforms_spec.lua @@ -130,7 +130,7 @@ describe("util.dataforms", function () 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 --git a/spec/util_datamapper_spec.lua b/spec/util_datamapper_spec.lua index 51ccf127..cc0e80e1 100644 --- a/spec/util_datamapper_spec.lua +++ b/spec/util_datamapper_spec.lua @@ -15,22 +15,22 @@ describe("util.datamapper", function() 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 @@ describe("util.datamapper", function() xml = {name = "stanza-id"; namespace = "urn:xmpp:sid:0"}; type = "object"; properties = { - id = attr(); - by = attr(); + id = attr; + by = attr; }; }; }; @@ -120,10 +120,10 @@ describe("util.datamapper", function() 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 --git a/spec/util_datetime_spec.lua b/spec/util_datetime_spec.lua index 497ab7d3..960e8aef 100644 --- a/spec/util_datetime_spec.lua +++ b/spec/util_datetime_spec.lua @@ -16,7 +16,10 @@ describe("util.datetime", function () 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 @@ describe("util.datetime", function () assert.truthy(string.find(time(), "^%d%d:%d%d:%d%d")); end); it("should work", function () - assert.equals(time(1136239445), "22:04:05"); - end); + 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 @@ describe("util.datetime", function () 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"); - end); + 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 @@ describe("util.datetime", function () 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 --git a/spec/util_dbuffer_spec.lua b/spec/util_dbuffer_spec.lua index 83ca1823..0072a59d 100644 --- a/spec/util_dbuffer_spec.lua +++ b/spec/util_dbuffer_spec.lua @@ -6,6 +6,8 @@ describe("util.dbuffer", function () 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 @@ describe("util.dbuffer", function () 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 @@ describe("util.dbuffer", function () 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 @@ describe("util.dbuffer", function () 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 @@ describe("util.dbuffer", function () 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 @@ describe("util.dbuffer", function () 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 @@ describe("util.dbuffer", function () 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 --git a/spec/util_error_spec.lua b/spec/util_error_spec.lua index be176635..cd30ee94 100644 --- a/spec/util_error_spec.lua +++ b/spec/util_error_spec.lua @@ -29,10 +29,10 @@ describe("util.error", function () end); - describe("is_err()", function () + describe("is_error()", function () it("works", function () - assert.truthy(errors.is_err(errors.new())); - assert.falsy(errors.is_err("not an error")); + assert.truthy(errors.is_error(errors.new())); + assert.falsy(errors.is_error("not an error")); end); end); @@ -40,7 +40,7 @@ describe("util.error", function () it("works", function () local ok, err = errors.coerce(nil, "it dun goofed"); assert.is_nil(ok); - assert.truthy(errors.is_err(err)) + assert.truthy(errors.is_error(err)) end); end); @@ -50,12 +50,15 @@ describe("util.error", function () local m = st.message({ type = "chat" }); local e = st.error_reply(m, "modify", "bad-request", nil, "error.example"):tag("extra", { xmlns = "xmpp:example.test" }); local err = errors.from_stanza(e); - assert.truthy(errors.is_err(err)); + assert.truthy(errors.is_error(err)); assert.equal("modify", err.type); assert.equal("bad-request", err.condition); 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); @@ -184,7 +187,7 @@ describe("util.error", function () end local ok, err = reg.coerce(test()); assert.is_nil(ok); - assert.is_truthy(errors.is_err(err)); + assert.is_truthy(errors.is_error(err)); assert.equal("forbidden", err.condition); end); @@ -205,7 +208,7 @@ describe("util.error", function () end local ok, err = reg.coerce(test()); assert.is_nil(ok); - assert.is_truthy(errors.is_err(err)); + assert.is_truthy(errors.is_error(err)); assert.equal("internal-server-error", err.condition); assert.equal("Oh no", err.text); end); diff --git a/spec/util_format_spec.lua b/spec/util_format_spec.lua index cb473b47..5ea0bdcf 100644 --- a/spec/util_format_spec.lua +++ b/spec/util_format_spec.lua @@ -13,7 +13,7 @@ describe("util.format", function() 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 @@ describe("util.format", function() 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("util.format", function() 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("util.format", function() 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("util.format", function() 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 --git a/spec/util_fsm_spec.lua b/spec/util_fsm_spec.lua new file mode 100644 index 00000000..9aff7ca6 --- /dev/null +++ b/spec/util_fsm_spec.lua @@ -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 --git a/spec/util_hashes_spec.lua b/spec/util_hashes_spec.lua index 51a4a79c..c4b1841e 100644 --- a/spec/util_hashes_spec.lua +++ b/spec/util_hashes_spec.lua @@ -4,6 +4,8 @@ local hex = require "util.hex"; -- 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 @@ describe("PBKDF2-HMAC-SHA256", function () 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 --git a/spec/util_hashring_spec.lua b/spec/util_hashring_spec.lua index d8801774..4f6ec3a3 100644 --- a/spec/util_hashring_spec.lua +++ b/spec/util_hashring_spec.lua @@ -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 @@ describe("util.hashring", function () 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 --git a/spec/util_http_spec.lua b/spec/util_http_spec.lua index c6087450..3e2f5a6a 100644 --- a/spec/util_http_spec.lua +++ b/spec/util_http_spec.lua @@ -41,6 +41,7 @@ describe("util.http", function() 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 @@ describe("util.http", function() 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 --git a/spec/util_human_io_spec.lua b/spec/util_human_io_spec.lua index f1b28883..91a87b39 100644 --- a/spec/util_human_io_spec.lua +++ b/spec/util_human_io_spec.lua @@ -42,6 +42,64 @@ describe("util.human.io", function () 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 --git a/spec/util_ip_spec.lua b/spec/util_ip_spec.lua index be5e4cff..a0287ee7 100644 --- a/spec/util_ip_spec.lua +++ b/spec/util_ip_spec.lua @@ -36,6 +36,8 @@ describe("util.ip", function() 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 @@ describe("util.ip", function() 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 --git a/spec/util_iterators_spec.lua b/spec/util_iterators_spec.lua index 4cf6f19d..a4b85884 100644 --- a/spec/util_iterators_spec.lua +++ b/spec/util_iterators_spec.lua @@ -10,6 +10,14 @@ describe("util.iterators", function () 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 --git a/spec/util_jid_spec.lua b/spec/util_jid_spec.lua index b92ca06c..4f5fe356 100644 --- a/spec/util_jid_spec.lua +++ b/spec/util_jid_spec.lua @@ -48,6 +48,47 @@ describe("util.jid", function() 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("<invalid node>@server", nil, nil, nil); + test("node@server/invalid-\000-resource", nil, nil, nil); + end); + end); + describe("#bare()", function() it("should work", function() diff --git a/spec/util_jsonpointer_spec.lua b/spec/util_jsonpointer_spec.lua index ce07c7a1..75122296 100644 --- a/spec/util_jsonpointer_spec.lua +++ b/spec/util_jsonpointer_spec.lua @@ -21,9 +21,11 @@ describe("util.jsonpointer", function() }]]) 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 --git a/spec/util_jsonschema_spec.lua b/spec/util_jsonschema_spec.lua index 968acaf1..cd4bcadd 100644 --- a/spec/util_jsonschema_spec.lua +++ b/spec/util_jsonschema_spec.lua @@ -2,7 +2,7 @@ local js = require "util.jsonschema"; 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 @@ local skip = { ["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 @@ local skip = { ["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 @@ local skip = { ["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 --git a/spec/util_jwt_spec.lua b/spec/util_jwt_spec.lua index b391a870..6946bdd3 100644 --- a/spec/util_jwt_spec.lua +++ b/spec/util_jwt_spec.lua @@ -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 @@ describe("util.jwt", function () 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 @@ describe("util.jwt", function () 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 --git a/spec/util_paseto_spec.lua b/spec/util_paseto_spec.lua new file mode 100644 index 00000000..417278b1 --- /dev/null +++ b/spec/util_paseto_spec.lua @@ -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 --git a/spec/util_poll_spec.lua b/spec/util_poll_spec.lua index a763be90..05318453 100644 --- a/spec/util_poll_spec.lua +++ b/spec/util_poll_spec.lua @@ -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 --git a/spec/util_promise_spec.lua b/spec/util_promise_spec.lua index 597b56f8..91b3d2d1 100644 --- a/spec/util_promise_spec.lua +++ b/spec/util_promise_spec.lua @@ -7,6 +7,11 @@ describe("util.promise", function () 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 @@ describe("util.promise", function () 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 @@ describe("util.promise", function () { 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 --git a/spec/util_pubsub_spec.lua b/spec/util_pubsub_spec.lua index 45a612a0..a03ffa64 100644 --- a/spec/util_pubsub_spec.lua +++ b/spec/util_pubsub_spec.lua @@ -108,7 +108,7 @@ describe("util.pubsub", function () it("fails to publish to a node with differing config", function () local ok, err = service:publish("node", true, "1", "item 2", { myoption = false }); assert.falsy(ok); - assert.equals("precondition-not-met", err.pubsub_condition); + assert.equals("precondition-not-met", err); end); it("allows to publish to a node with differing config when only defaults are suggested", function () @@ -605,4 +605,14 @@ describe("util.pubsub", function () end); end) + + describe("metadata", function() + it("works", function() + local service = pubsub.new { metadata_subset = { "title" } }; + assert.truthy(service:create("node", true, { title = "Hello", secret = "hidden" })) + local ok, meta = service:get_node_metadata("node", "nobody"); + assert.truthy(ok, meta); + assert.same({ title = "Hello" }, meta); + end) + end); end); diff --git a/spec/util_queue_spec.lua b/spec/util_queue_spec.lua index d73f523d..d9e92e3d 100644 --- a/spec/util_queue_spec.lua +++ b/spec/util_queue_spec.lua @@ -137,4 +137,23 @@ describe("util.queue", function() assert.equal(c, 6); end); end); + describe("replace()", function () + it("should work", function () + local q = queue.new(10); + for i = 1, 5 do + q:push(i); + end + q:replace(6); + local c = 0; + for i in q:consume() do + c = c + 1; + if c > 1 then + assert.is_equal(c, i); + elseif c == 1 then + assert.is_equal(6, i); + end + end + assert.is_equal(5, c); + end); + end); end); diff --git a/spec/util_rfc6724_spec.lua b/spec/util_rfc6724_spec.lua deleted file mode 100644 index 30e935b6..00000000 --- a/spec/util_rfc6724_spec.lua +++ /dev/null @@ -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 --git a/spec/util_roles_spec.lua b/spec/util_roles_spec.lua new file mode 100644 index 00000000..44d2a977 --- /dev/null +++ b/spec/util_roles_spec.lua @@ -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 = { + -- { <final_role policy>, <parent_role policy>, <test_role policy> } + { 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 --git a/spec/util_sasl_spec.lua b/spec/util_sasl_spec.lua index 368291b3..67b60758 100644 --- a/spec/util_sasl_spec.lua +++ b/spec/util_sasl_spec.lua @@ -39,5 +39,37 @@ describe("util.sasl", function () -- 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 --git a/spec/util_smqueue_spec.lua b/spec/util_smqueue_spec.lua index 0a02a60b..858b99c0 100644 --- a/spec/util_smqueue_spec.lua +++ b/spec/util_smqueue_spec.lua @@ -5,6 +5,9 @@ describe("util.smqueue", function() 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 --git a/spec/util_stanza_spec.lua b/spec/util_stanza_spec.lua index 8732a111..f79f8cb9 100644 --- a/spec/util_stanza_spec.lua +++ b/spec/util_stanza_spec.lua @@ -314,6 +314,20 @@ describe("util.stanza", function() 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"] = "<>"; @@ -563,5 +577,10 @@ describe("util.stanza", function() assert.equal("text", s:find("{urn:example:not:same}child/nested#"), "finds nested text") assert.is_nil(s:find("child"), "respects namespaces") end); + it("handles namespaced attributes", function() + local s = st.stanza("root", { ["urn:example:namespace\1attr"] = "value" }, { e = "urn:example:namespace" }); + assert.equal("value", s:find("@e:attr"), "finds prefixed attr") + assert.equal("value", s:find("@{urn:example:namespace}attr"), "finds clark attr") + end) end); end); diff --git a/spec/util_strbitop.lua b/spec/util_strbitop_spec.lua index 58a13772..963c9516 100644 --- a/spec/util_strbitop.lua +++ b/spec/util_strbitop_spec.lua @@ -38,4 +38,48 @@ describe("util.strbitop", 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 --git a/spec/util_table_spec.lua b/spec/util_table_spec.lua index 76f54b69..574967f3 100644 --- a/spec/util_table_spec.lua +++ b/spec/util_table_spec.lua @@ -12,6 +12,48 @@ describe("util.table", function () 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 --git a/spec/util_throttle_spec.lua b/spec/util_throttle_spec.lua index 985afae8..077f3d40 100644 --- a/spec/util_throttle_spec.lua +++ b/spec/util_throttle_spec.lua @@ -1,4 +1,5 @@ +-- luacheck: ignore 411/a -- Mock util.time local now = 0; -- wibbly-wobbly... timey-wimey... stuff diff --git a/spec/util_uuid_spec.lua b/spec/util_uuid_spec.lua index 95ae0a20..7157d0ec 100644 --- a/spec/util_uuid_spec.lua +++ b/spec/util_uuid_spec.lua @@ -5,7 +5,7 @@ local uuid = require "util.uuid"; 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 @@ describe("util.uuid", function() 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 --git a/spec/util_xtemplate_spec.lua b/spec/util_xtemplate_spec.lua new file mode 100644 index 00000000..3b0ecaa9 --- /dev/null +++ b/spec/util_xtemplate_spec.lua @@ -0,0 +1,46 @@ +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" }, {["pubsub"] = "http://jabber.org/protocol/pubsub"}); + 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["http://jabber.org/protocol/pubsub\1title"] = "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) + it("handles missing inputs", function () + local x = st.stanza("root"); + assert.same("", xtemplate.render("{foo/bar|each(i){{#}}}", x)); + end) + end) + end) +end) diff --git a/teal-src/README.md b/teal-src/README.md new file mode 100644 index 00000000..e8c47035 --- /dev/null +++ b/teal-src/README.md @@ -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 --git a/teal-src/module.d.tl b/teal-src/module.d.tl index 67b2437c..22038bc4 100644 --- a/teal-src/module.d.tl +++ b/teal-src/module.d.tl @@ -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 @@ global record moduleapi get_option : config_getter<any> get_option_scalar : config_getter<nil | boolean | number | string> get_option_string : config_getter<string> - get_option_number : config_getter<number> + 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<boolean> + get_option_enum : function<A> (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 @@ global record moduleapi 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 @@ global record moduleapi load_resource : function (moduleapi, string, file_mode) : FILE enum store_type "keyval" + "keyval+" "map" "archive" end @@ -121,12 +130,20 @@ global record moduleapi 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 --git a/teal-src/plugins/mod_cron.tl b/teal-src/plugins/mod_cron.tl deleted file mode 100644 index f3b8f62f..00000000 --- a/teal-src/plugins/mod_cron.tl +++ /dev/null @@ -1,106 +0,0 @@ -module:set_global(); - -local async = require "util.async"; -local datetime = require "util.datetime"; - -local record map_store<K,V> - -- TODO move to somewhere sensible - get : function (map_store<K,V>, string, K) : V - set : function (map_store<K,V>, 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<string,integer>; - 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 --git a/teal-src/prosody/core/storagemanager.d.tl b/teal-src/prosody/core/storagemanager.d.tl new file mode 100644 index 00000000..bd82357c --- /dev/null +++ b/teal-src/prosody/core/storagemanager.d.tl @@ -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 --git a/teal-src/prosody/core/usermanager.d.tl b/teal-src/prosody/core/usermanager.d.tl new file mode 100644 index 00000000..fc055004 --- /dev/null +++ b/teal-src/prosody/core/usermanager.d.tl @@ -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 --git a/teal-src/prosody/net/http.d.tl b/teal-src/prosody/net/http.d.tl new file mode 100644 index 00000000..43b0bf4d --- /dev/null +++ b/teal-src/prosody/net/http.d.tl @@ -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 --git a/teal-src/prosody/net/http/codes.d.tl b/teal-src/prosody/net/http/codes.d.tl new file mode 100644 index 00000000..65d004fc --- /dev/null +++ b/teal-src/prosody/net/http/codes.d.tl @@ -0,0 +1,2 @@ +local type response_codes = { integer : string } +return response_codes diff --git a/teal-src/prosody/net/http/errors.d.tl b/teal-src/prosody/net/http/errors.d.tl new file mode 100644 index 00000000..a9b6ea6c --- /dev/null +++ b/teal-src/prosody/net/http/errors.d.tl @@ -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 --git a/teal-src/prosody/net/http/files.d.tl b/teal-src/prosody/net/http/files.d.tl new file mode 100644 index 00000000..d0ba5c1c --- /dev/null +++ b/teal-src/prosody/net/http/files.d.tl @@ -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 --git a/teal-src/prosody/net/http/parser.d.tl b/teal-src/prosody/net/http/parser.d.tl new file mode 100644 index 00000000..1cd6ccf4 --- /dev/null +++ b/teal-src/prosody/net/http/parser.d.tl @@ -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 --git a/teal-src/prosody/net/http/server.d.tl b/teal-src/prosody/net/http/server.d.tl new file mode 100644 index 00000000..5a83af7e --- /dev/null +++ b/teal-src/prosody/net/http/server.d.tl @@ -0,0 +1,6 @@ + +local record http_server + -- TODO +end + +return http_server diff --git a/teal-src/prosody/net/server.d.tl b/teal-src/prosody/net/server.d.tl new file mode 100644 index 00000000..bb61f677 --- /dev/null +++ b/teal-src/prosody/net/server.d.tl @@ -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 --git a/teal-src/prosody/plugins/mod_cron.tl b/teal-src/prosody/plugins/mod_cron.tl new file mode 100644 index 00000000..4defc808 --- /dev/null +++ b/teal-src/prosody/plugins/mod_cron.tl @@ -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<K,V> + -- TODO move to somewhere sensible + get : function (map_store<K,V>, string, K) : V + set : function (map_store<K,V>, 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<string,integer>; + 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<task_spec> = 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 --git a/teal-src/prosody/plugins/muc/muc.lib.d.tl b/teal-src/prosody/plugins/muc/muc.lib.d.tl new file mode 100644 index 00000000..6e9663a1 --- /dev/null +++ b/teal-src/prosody/plugins/muc/muc.lib.d.tl @@ -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 --git a/teal-src/prosody/util/array.d.tl b/teal-src/prosody/util/array.d.tl new file mode 100644 index 00000000..70bf2624 --- /dev/null +++ b/teal-src/prosody/util/array.d.tl @@ -0,0 +1,9 @@ +local record array_t<T> + { T } +end + +local record lib + metamethod __call : function () : array_t +end + +return lib diff --git a/teal-src/prosody/util/async.d.tl b/teal-src/prosody/util/async.d.tl new file mode 100644 index 00000000..a2e41cd6 --- /dev/null +++ b/teal-src/prosody/util/async.d.tl @@ -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<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<T>, ... : any) + type watchers_t = { state_e : watcher_t } + data : any + id : string + + run : function (runner_t<T>, T) : boolean, state_e, integer + enqueue : function (runner_t<T>, T) : runner_t<T> + log : function (runner_t<T>, string, string, ... : any) + onready : function (runner_t<T>, function) : runner_t<T> + onready : function (runner_t<T>, function) : runner_t<T> + onwaiting : function (runner_t<T>, function) : runner_t<T> + onerror : function (runner_t<T>, function) : runner_t<T> + end + runner : function <T>(function (T), runner_t.watchers_t, any) : runner_t<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 --git a/teal-src/prosody/util/bitcompat.d.tl b/teal-src/prosody/util/bitcompat.d.tl new file mode 100644 index 00000000..18adf725 --- /dev/null +++ b/teal-src/prosody/util/bitcompat.d.tl @@ -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 --git a/teal-src/util/compat.d.tl b/teal-src/prosody/util/compat.d.tl index da9c6083..da9c6083 100644 --- a/teal-src/util/compat.d.tl +++ b/teal-src/prosody/util/compat.d.tl diff --git a/teal-src/util/crand.d.tl b/teal-src/prosody/util/crand.d.tl index b40cb67e..b40cb67e 100644 --- a/teal-src/util/crand.d.tl +++ b/teal-src/prosody/util/crand.d.tl diff --git a/teal-src/prosody/util/crypto.d.tl b/teal-src/prosody/util/crypto.d.tl new file mode 100644 index 00000000..866185d0 --- /dev/null +++ b/teal-src/prosody/util/crypto.d.tl @@ -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 --git a/teal-src/prosody/util/dataforms.d.tl b/teal-src/prosody/util/dataforms.d.tl new file mode 100644 index 00000000..3dad2776 --- /dev/null +++ b/teal-src/prosody/util/dataforms.d.tl @@ -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 --git a/teal-src/util/datamapper.tl b/teal-src/prosody/util/datamapper.tl index 73b1dfc0..89d5e29b 100644 --- a/teal-src/util/datamapper.tl +++ b/teal-src/prosody/util/datamapper.tl @@ -19,12 +19,14 @@ -- 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"; +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 "util.jsonschema" +local json_schema_object = require "prosody.util.jsonschema" local type schema_t = boolean | json_schema_object local function toboolean ( s : string ) : boolean @@ -133,10 +135,6 @@ local function unpack_propschema( propschema : schema_t, propname : string, curr 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 diff --git a/teal-src/prosody/util/datetime.d.tl b/teal-src/prosody/util/datetime.d.tl new file mode 100644 index 00000000..9f770a73 --- /dev/null +++ b/teal-src/prosody/util/datetime.d.tl @@ -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 --git a/teal-src/util/encodings.d.tl b/teal-src/prosody/util/encodings.d.tl index 58aefa5e..58aefa5e 100644 --- a/teal-src/util/encodings.d.tl +++ b/teal-src/prosody/util/encodings.d.tl diff --git a/teal-src/util/error.d.tl b/teal-src/prosody/util/error.d.tl index 05f52405..4c3a7196 100644 --- a/teal-src/util/error.d.tl +++ b/teal-src/prosody/util/error.d.tl @@ -38,7 +38,7 @@ local record protoerror code : integer end -local record error +local record Error type : error_type condition : error_condition text : string @@ -55,10 +55,10 @@ 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 + 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 @@ -66,12 +66,12 @@ local record lib record configure_opt auto_inject_traceback : boolean end - new : function (protoerror, context, { string : protoerror }, string) : error + 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 + coerce : function (any, string) : any, Error + from_stanza : function (table, context, string) : Error configure : function end diff --git a/teal-src/util/format.d.tl b/teal-src/prosody/util/format.d.tl index 1ff77c97..1ff77c97 100644 --- a/teal-src/util/format.d.tl +++ b/teal-src/prosody/util/format.d.tl diff --git a/teal-src/util/hashes.d.tl b/teal-src/prosody/util/hashes.d.tl index cbb06f8e..64c5a12b 100644 --- a/teal-src/util/hashes.d.tl +++ b/teal-src/prosody/util/hashes.d.tl @@ -4,18 +4,30 @@ local type kdf = function (pass : string, salt : string, i : integer) : string local record lib sha1 : hash - sha256 : 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 diff --git a/teal-src/util/hex.d.tl b/teal-src/prosody/util/hex.d.tl index 3b216a88..9d84540b 100644 --- a/teal-src/util/hex.d.tl +++ b/teal-src/prosody/util/hex.d.tl @@ -2,5 +2,7 @@ local type s2s = function (s : string) : string local record lib to : s2s from : s2s + encode : s2s + decode : s2s end return lib diff --git a/teal-src/util/http.d.tl b/teal-src/prosody/util/http.d.tl index ecbe35c3..ecbe35c3 100644 --- a/teal-src/util/http.d.tl +++ b/teal-src/prosody/util/http.d.tl diff --git a/teal-src/prosody/util/human/io.d.tl b/teal-src/prosody/util/human/io.d.tl new file mode 100644 index 00000000..e4f64cd1 --- /dev/null +++ b/teal-src/prosody/util/human/io.d.tl @@ -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<K,V> + 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> ({ K : V }) : string + table : function<K,V> ({ tablerow<K,V> }, width : integer) : getrow<K,V> +end + +return lib diff --git a/teal-src/util/human/units.d.tl b/teal-src/prosody/util/human/units.d.tl index f6568d90..3db17c3a 100644 --- a/teal-src/util/human/units.d.tl +++ b/teal-src/prosody/util/human/units.d.tl @@ -1,5 +1,8 @@ local lib = record + enum logbase + "b" -- 1024 + end adjust : function (number, string) : number, string - format : function (number, string, string) : string + format : function (number, string, logbase) : string end return lib diff --git a/teal-src/util/id.d.tl b/teal-src/prosody/util/id.d.tl index 4b6c93b7..4b6c93b7 100644 --- a/teal-src/util/id.d.tl +++ b/teal-src/prosody/util/id.d.tl diff --git a/teal-src/util/interpolation.d.tl b/teal-src/prosody/util/interpolation.d.tl index fb653edf..fb653edf 100644 --- a/teal-src/util/interpolation.d.tl +++ b/teal-src/prosody/util/interpolation.d.tl diff --git a/teal-src/prosody/util/ip.d.tl b/teal-src/prosody/util/ip.d.tl new file mode 100644 index 00000000..79e901c6 --- /dev/null +++ b/teal-src/prosody/util/ip.d.tl @@ -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 --git a/teal-src/util/jid.d.tl b/teal-src/prosody/util/jid.d.tl index 897318d9..897318d9 100644 --- a/teal-src/util/jid.d.tl +++ b/teal-src/prosody/util/jid.d.tl diff --git a/teal-src/util/json.d.tl b/teal-src/prosody/util/json.d.tl index a1c25004..a1c25004 100644 --- a/teal-src/util/json.d.tl +++ b/teal-src/prosody/util/json.d.tl diff --git a/teal-src/util/jsonpointer.tl b/teal-src/prosody/util/jsonpointer.tl index c21e1fbf..5ea99732 100644 --- a/teal-src/util/jsonpointer.tl +++ b/teal-src/prosody/util/jsonpointer.tl @@ -9,7 +9,7 @@ local function unescape_token(escaped_token : string) : string return unescaped end -local function resolve_json_pointer(ref : table, path : string) : any, ptr_error +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) @@ -23,7 +23,7 @@ local function resolve_json_pointer(ref : table, path : string) : any, ptr_error new_ref = ref[token] elseif idx is integer then local i = tonumber(token) - if token == "-" then i = #ref + 1 end + if token == "-" then i = #(ref as {any}) + 1 end new_ref = ref[i+1] else return nil, "invalid-table" diff --git a/teal-src/prosody/util/jsonschema.tl b/teal-src/prosody/util/jsonschema.tl new file mode 100644 index 00000000..1cc2314f --- /dev/null +++ b/teal-src/prosody/util/jsonschema.tl @@ -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 --git a/teal-src/prosody/util/jwt.d.tl b/teal-src/prosody/util/jwt.d.tl new file mode 100644 index 00000000..b3d0cd9e --- /dev/null +++ b/teal-src/prosody/util/jwt.d.tl @@ -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 --git a/teal-src/prosody/util/logger.d.tl b/teal-src/prosody/util/logger.d.tl new file mode 100644 index 00000000..db29adfd --- /dev/null +++ b/teal-src/prosody/util/logger.d.tl @@ -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 --git a/teal-src/prosody/util/mathcompat.tl b/teal-src/prosody/util/mathcompat.tl new file mode 100644 index 00000000..1e3f9bab --- /dev/null +++ b/teal-src/prosody/util/mathcompat.tl @@ -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 --git a/teal-src/util/net.d.tl b/teal-src/prosody/util/net.d.tl index 1040fcef..1040fcef 100644 --- a/teal-src/util/net.d.tl +++ b/teal-src/prosody/util/net.d.tl diff --git a/teal-src/util/poll.d.tl b/teal-src/prosody/util/poll.d.tl index 8df56d57..8df56d57 100644 --- a/teal-src/util/poll.d.tl +++ b/teal-src/prosody/util/poll.d.tl diff --git a/teal-src/util/pposix.d.tl b/teal-src/prosody/util/pposix.d.tl index 68f49730..d7780835 100644 --- a/teal-src/util/pposix.d.tl +++ b/teal-src/prosody/util/pposix.d.tl @@ -97,6 +97,7 @@ local record pposix meminfo : function () : memoryinfo atomic_append : function (f : FILE, s : string) : boolean, string, integer + remove_blocks : function (f : FILE, integer, integer) isatty : function(FILE) : boolean diff --git a/teal-src/prosody/util/promise.d.tl b/teal-src/prosody/util/promise.d.tl new file mode 100644 index 00000000..d895a828 --- /dev/null +++ b/teal-src/prosody/util/promise.d.tl @@ -0,0 +1,22 @@ + +local record lib + type resolve_func = function (any) + type promise_body = function (resolve_func, resolve_func) + + record Promise<A, B> + type on_resolved = function (A) : any + type on_rejected = function (B) : any + next : function (Promise, on_resolved, on_rejected) : Promise<any, any> + 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 --git a/teal-src/prosody/util/queue.d.tl b/teal-src/prosody/util/queue.d.tl new file mode 100644 index 00000000..cb8458e7 --- /dev/null +++ b/teal-src/prosody/util/queue.d.tl @@ -0,0 +1,21 @@ +local record lib + record queue<T> + size : integer + count : function (queue<T>) : integer + enum push_errors + "queue full" + end + + push : function (queue<T>, T) : boolean, push_errors + pop : function (queue<T>) : T + peek : function (queue<T>) : T + replace : function (queue<T>, T) : boolean, push_errors + type iterator = function (T, integer) : integer, T + items : function (queue<T>) : iterator, T, integer + type consume_iter = function (queue<T>) : T + consume : function (queue<T>) : consume_iter + end + + new : function<T> (size:integer, allow_wrapping:boolean) : queue<T> +end +return lib; diff --git a/teal-src/util/random.d.tl b/teal-src/prosody/util/random.d.tl index 83ff2fcc..83ff2fcc 100644 --- a/teal-src/util/random.d.tl +++ b/teal-src/prosody/util/random.d.tl diff --git a/teal-src/util/ringbuffer.d.tl b/teal-src/prosody/util/ringbuffer.d.tl index e4726d68..e4726d68 100644 --- a/teal-src/util/ringbuffer.d.tl +++ b/teal-src/prosody/util/ringbuffer.d.tl diff --git a/teal-src/prosody/util/roles.d.tl b/teal-src/prosody/util/roles.d.tl new file mode 100644 index 00000000..fef4f88a --- /dev/null +++ b/teal-src/prosody/util/roles.d.tl @@ -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 --git a/teal-src/prosody/util/serialization.d.tl b/teal-src/prosody/util/serialization.d.tl new file mode 100644 index 00000000..528c9705 --- /dev/null +++ b/teal-src/prosody/util/serialization.d.tl @@ -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 --git a/teal-src/prosody/util/set.d.tl b/teal-src/prosody/util/set.d.tl new file mode 100644 index 00000000..a23dd258 --- /dev/null +++ b/teal-src/prosody/util/set.d.tl @@ -0,0 +1,22 @@ +local record lib + record Set<T> + add : function<T> (Set<T>, T) + contains : function<T> (Set<T>, T) : boolean + contains_set : function<T> (Set<T>, Set<T>) : boolean + items : function<T> (Set<T>) : function<T> (Set<T>, T) : T + remove : function<T> (Set<T>, T) + add_list : function<T> (Set<T>, { T }) + include : function<T> (Set<T>, Set<T>) + exclude : function<T> (Set<T>, Set<T>) + empty : function<T> (Set<T>) : boolean + end + + new : function<T> ({ T }) : Set<T> + is_set : function (any) : boolean + union : function<T> (Set<T>, Set<T>) : Set <T> + difference : function<T> (Set<T>, Set<T>) : Set <T> + intersection : function<T> (Set<T>, Set<T>) : Set <T> + xor : function<T> (Set<T>, Set<T>) : Set <T> +end + +return lib diff --git a/teal-src/util/signal.d.tl b/teal-src/prosody/util/signal.d.tl index 8610aa7f..290cf08f 100644 --- a/teal-src/util/signal.d.tl +++ b/teal-src/prosody/util/signal.d.tl @@ -1,5 +1,5 @@ local record lib - enum signal + enum Signal "SIGABRT" "SIGALRM" "SIGBUS" @@ -33,9 +33,9 @@ local record lib "SIGXCPU" "SIGXFSZ" end - signal : function (integer | signal, function, boolean) : boolean - raise : function (integer | signal) - kill : function (integer, integer | signal) + signal : function (integer | Signal, function, boolean) : boolean + raise : function (integer | Signal) + kill : function (integer, integer | Signal) -- enum : integer end return lib diff --git a/teal-src/util/smqueue.tl b/teal-src/prosody/util/smqueue.tl index e149dde7..821aee57 100644 --- a/teal-src/util/smqueue.tl +++ b/teal-src/prosody/util/smqueue.tl @@ -1,4 +1,4 @@ -local queue = require "util.queue"; +local queue = require "prosody.util.queue"; local record lib -- T would typically be util.stanza diff --git a/teal-src/util/stanza.d.tl b/teal-src/prosody/util/stanza.d.tl index a358248a..e1ab2105 100644 --- a/teal-src/util/stanza.d.tl +++ b/teal-src/prosody/util/stanza.d.tl @@ -4,6 +4,39 @@ local record lib 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 } @@ -16,6 +49,7 @@ local record lib 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 ) @@ -24,6 +58,8 @@ local record lib 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 @@ -35,7 +71,9 @@ local record lib pretty_print : function ( stanza_t ) : string pretty_top_tag : function ( stanza_t ) : string - get_error : function ( stanza_t ) : string, string, string, stanza_t + -- 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 @@ -45,16 +83,61 @@ local record lib { 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 ( { string : string }, string ) : stanza_t - iq : function ( { string : string } ) : 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, string, string, string, string ) - presence : function ( { string : string } ) : 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 diff --git a/teal-src/util/strbitop.d.tl b/teal-src/prosody/util/strbitop.d.tl index 010efdb8..86577ef2 100644 --- a/teal-src/util/strbitop.d.tl +++ b/teal-src/prosody/util/strbitop.d.tl @@ -2,5 +2,6 @@ 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 --git a/teal-src/prosody/util/struct.d.tl b/teal-src/prosody/util/struct.d.tl new file mode 100644 index 00000000..201aaa23 --- /dev/null +++ b/teal-src/prosody/util/struct.d.tl @@ -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 --git a/teal-src/util/table.d.tl b/teal-src/prosody/util/table.d.tl index 0ff5ed95..67e5d0f0 100644 --- a/teal-src/util/table.d.tl +++ b/teal-src/prosody/util/table.d.tl @@ -1,6 +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 --git a/teal-src/prosody/util/termcolours.d.tl b/teal-src/prosody/util/termcolours.d.tl new file mode 100644 index 00000000..226259aa --- /dev/null +++ b/teal-src/prosody/util/termcolours.d.tl @@ -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 --git a/teal-src/util/time.d.tl b/teal-src/prosody/util/time.d.tl index e159706b..e159706b 100644 --- a/teal-src/util/time.d.tl +++ b/teal-src/prosody/util/time.d.tl diff --git a/teal-src/prosody/util/timer.d.tl b/teal-src/prosody/util/timer.d.tl new file mode 100644 index 00000000..a6394cf3 --- /dev/null +++ b/teal-src/prosody/util/timer.d.tl @@ -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 --git a/teal-src/util/uuid.d.tl b/teal-src/prosody/util/uuid.d.tl index 45fd4312..284a4e4c 100644 --- a/teal-src/util/uuid.d.tl +++ b/teal-src/prosody/util/uuid.d.tl @@ -1,5 +1,5 @@ local record lib - get_nibbles : (number) : string + get_nibbles : function (number) : string generate : function () : string seed : function (string) diff --git a/teal-src/util/xtemplate.tl b/teal-src/prosody/util/xtemplate.tl index cdc913bf..4d293359 100644 --- a/teal-src/util/xtemplate.tl +++ b/teal-src/prosody/util/xtemplate.tl @@ -14,17 +14,25 @@ local s_match = string.match; local s_sub = string.sub; local t_concat = table.concat; -local st = require "util.stanza"; +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_t = function (string | st.stanza_t, 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 + 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 @@ -48,7 +56,7 @@ local function render(template : string, root : st.stanza_t, escape : escape_t, if func == "each" and tmpl then if not st.is_stanza(value) then - return ""; + return pre_blank..post_blank; end if not args then value, args = root, path; end local ns, name = s_match(args, "^(%b{})(.*)$"); @@ -77,11 +85,7 @@ local function render(template : string, root : st.stanza_t, escape : escape_t, 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 + value, is_escaped = f(value, args, tmpl); else error("No such filter function: " .. func); end @@ -90,14 +94,14 @@ local function render(template : string, root : st.stanza_t, escape : escape_t, if value is 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); + return pre_blank .. escape(value) .. post_blank; end end - return ""; + return pre_blank .. post_blank; end)); end diff --git a/teal-src/util/dataforms.d.tl b/teal-src/util/dataforms.d.tl deleted file mode 100644 index 9e4170fa..00000000 --- a/teal-src/util/dataforms.d.tl +++ /dev/null @@ -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 --git a/teal-src/util/datetime.d.tl b/teal-src/util/datetime.d.tl deleted file mode 100644 index 971e8f9c..00000000 --- a/teal-src/util/datetime.d.tl +++ /dev/null @@ -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 --git a/teal-src/util/jsonschema.tl b/teal-src/util/jsonschema.tl deleted file mode 100644 index 160c164c..00000000 --- a/teal-src/util/jsonschema.tl +++ /dev/null @@ -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 --git a/tools/build-env/Containerfile b/tools/build-env/Containerfile new file mode 100644 index 00000000..6ba02ba0 --- /dev/null +++ b/tools/build-env/Containerfile @@ -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 --git a/tools/build-env/build.sh b/tools/build-env/build.sh new file mode 100755 index 00000000..4351f8b8 --- /dev/null +++ b/tools/build-env/build.sh @@ -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 --git a/tools/build-env/here.sh b/tools/build-env/here.sh new file mode 100755 index 00000000..1d5cb515 --- /dev/null +++ b/tools/build-env/here.sh @@ -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 --git a/tools/cfgdump.lua b/tools/cfgdump.lua index 8c331a58..eef13e6d 100755 --- a/tools/cfgdump.lua +++ b/tools/cfgdump.lua @@ -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 --git a/tools/dnsregistry.lua b/tools/dnsregistry.lua index 3fd26628..5a258c1a 100644 --- a/tools/dnsregistry.lua +++ b/tools/dnsregistry.lua @@ -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 @@ for registry in registries:childtags("registry") do 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 --git a/tools/ejabberdsql2prosody.lua b/tools/ejabberdsql2prosody.lua index d0ab71cf..272da0aa 100644 --- a/tools/ejabberdsql2prosody.lua +++ b/tools/ejabberdsql2prosody.lua @@ -16,13 +16,16 @@ if my_name:match("[/\\]") then 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 --git a/tools/form2table.lua b/tools/form2table.lua index 49a6972b..2d80ad23 100644 --- a/tools/form2table.lua +++ b/tools/form2table.lua @@ -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 @@ local function from_stanza(stanza) 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 --git a/tools/http-status-codes.lua b/tools/http-status-codes.lua index bd5bb52a..38f44992 100644 --- a/tools/http-status-codes.lua +++ b/tools/http-status-codes.lua @@ -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 --git a/tools/jabberd14sql2prosody.lua b/tools/jabberd14sql2prosody.lua index d1d66dc1..12658e50 100644 --- a/tools/jabberd14sql2prosody.lua +++ b/tools/jabberd14sql2prosody.lua @@ -1,4 +1,7 @@ #!/usr/bin/env lua +if not pcall(require, "prosody.loader") then + pcall(require, "loader"); +end do @@ -447,13 +450,13 @@ elseif package.config:sub(1,1) == "/" then 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 --git a/tools/migration/migrator.cfg.lua b/tools/migration/migrator.cfg.lua index b81389b3..6e29b9b8 100644 --- a/tools/migration/migrator.cfg.lua +++ b/tools/migration/migrator.cfg.lua @@ -3,6 +3,7 @@ local data_path = "../../data"; local vhost = { "accounts", "account_details", + "account_roles", "roster", "vcard", "private", @@ -12,18 +13,27 @@ local vhost = { "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 --git a/tools/migration/migrator/jabberd14.lua b/tools/migration/migrator/jabberd14.lua index a4eef3f7..7edb887c 100644 --- a/tools/migration/migrator/jabberd14.lua +++ b/tools/migration/migrator/jabberd14.lua @@ -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 --git a/tools/migration/prosody-migrator.lua b/tools/migration/prosody-migrator.lua index 21eb32e7..467ecf32 100644 --- a/tools/migration/prosody-migrator.lua +++ b/tools/migration/prosody-migrator.lua @@ -43,7 +43,11 @@ local function usage() 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 @@ end -- 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 @@ if have_err then 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 @@ local watchers = { 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 @@ local function prepare_config(host, conf) 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 @@ migrate_once.pubsub = function(origin, destination, user, prefix, input_driver, 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 --git a/tools/mod2spec.sh b/tools/mod2spec.sh new file mode 100755 index 00000000..98c0805a --- /dev/null +++ b/tools/mod2spec.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -eu + +echo "spec/${1//./_}_spec.lua" diff --git a/tools/modtrace.lua b/tools/modtrace.lua index 45fa9f6a..f1927077 100644 --- a/tools/modtrace.lua +++ b/tools/modtrace.lua @@ -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 --git a/tools/openfire2prosody.lua b/tools/openfire2prosody.lua index cd3e62e5..2d8c6867 100644 --- a/tools/openfire2prosody.lua +++ b/tools/openfire2prosody.lua @@ -15,6 +15,10 @@ if my_name:match("[/\\]") then 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 @@ elseif package.config:sub(1,1) == "/" then 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 --git a/tools/tb2err b/tools/tb2err index 7b676813..76c847dc 100755 --- a/tools/tb2err +++ b/tools/tb2err @@ -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 @@ for line in io.lines() do 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 --git a/tools/test_mutants.sh.lua b/tools/test_mutants.sh.lua new file mode 100755 index 00000000..6e2423db --- /dev/null +++ b/tools/test_mutants.sh.lua @@ -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 == "<integer>" 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 == "<file>" then return; end + if name == "<eof>" 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 == "<string>" 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 --git a/tools/xepchanges.sh b/tools/xepchanges.sh new file mode 100755 index 00000000..0a0140a1 --- /dev/null +++ b/tools/xepchanges.sh @@ -0,0 +1,14 @@ +#!/bin/sh -eu + +wget -N https://xmpp.org/extensions/xeplist.xml +xml2 <xeplist.xml | + 2csv xep-infos/xep number version | + grep -v ^xxxx,| + sort -g > 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 --git a/util-src/GNUmakefile b/util-src/GNUmakefile index 810f39f7..3f539387 100644 --- a/util-src/GNUmakefile +++ b/util-src/GNUmakefile @@ -8,7 +8,7 @@ TARGET?=../util/ 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 @@ clean: 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 --git a/util-src/compat.c b/util-src/compat.c index 34b35c35..3918bf3e 100644 --- a/util-src/compat.c +++ b/util-src/compat.c @@ -19,7 +19,7 @@ static int lc_xpcall (lua_State *L) { 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 @@ int luaopen_util_compat(lua_State *L) { } return 1; } + +int luaopen_util_compat(lua_State *L) { + return luaopen_prosody_util_compat(L); +} diff --git a/util-src/crand.c b/util-src/crand.c index 160ac1f6..6f17c162 100644 --- a/util-src/crand.c +++ b/util-src/crand.c @@ -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 @@ int getrandom(void *buf, size_t buflen, unsigned int flags) { #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 @@ int Lrandom(lua_State *L) { 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 @@ int luaopen_util_crand(lua_State *L) { return 1; } +int luaopen_util_crand(lua_State *L) { + return luaopen_prosody_util_crand(L); +} diff --git a/util-src/crypto.c b/util-src/crypto.c new file mode 100644 index 00000000..68733c7c --- /dev/null +++ b/util-src/crypto.c @@ -0,0 +1,738 @@ +/* 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 <string.h> +#include <stdlib.h> + +#ifdef _MSC_VER +typedef unsigned __int32 uint32_t; +#else +#include <inttypes.h> +#endif + +#include "lua.h" +#include "lauxlib.h" +#include <openssl/crypto.h> +#include <openssl/ecdsa.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/obj_mac.h> +#include <openssl/param_build.h> +#include <openssl/pem.h> + +#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 Lpkey_meth_derive(lua_State *L) { + size_t outlen; + EVP_PKEY *key = pkey_from_arg(L, 1, 0, 0); + EVP_PKEY *peer = pkey_from_arg(L, 2, 0, 0); + EVP_PKEY_CTX *ctx; + BUF_MEM *buf; + BIO *bio = new_managed_BIO_s_mem(L); + BIO_get_mem_ptr(bio, &buf); + if (!(ctx = EVP_PKEY_CTX_new(key, NULL))) + goto sslerr; + if (EVP_PKEY_derive_init(ctx) <= 0) + goto sslerr; + if (EVP_PKEY_derive_set_peer(ctx, peer) <= 0) + goto sslerr; + if (EVP_PKEY_derive(ctx, NULL, &outlen) <= 0) + goto sslerr; + if (!BUF_MEM_grow_clean(buf, outlen)) + goto sslerr; + if (EVP_PKEY_derive(ctx, (unsigned char*)buf->data, &outlen) <= 0) + goto sslerr; + EVP_PKEY_CTX_free(ctx); + ctx = NULL; + lua_pushlstring(L, buf->data, outlen); + BIO_reset(bio); + return 1; +sslerr: + if (ctx) { + EVP_PKEY_CTX_free(ctx); + ctx = NULL; + } + BIO_reset(bio); + return luaL_error(L, "pkey:derive failed"); +} + +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_raw(lua_State *L) { + OSSL_PARAM *params; + EVP_PKEY *pkey = pkey_from_arg(L, 1, 0, 0); + + if (EVP_PKEY_todata(pkey, EVP_PKEY_PUBLIC_KEY, ¶ms)) { + OSSL_PARAM *item = params; + while (item->key) { + if (!strcmp("pub", item->key)) { + lua_pushlstring(L, item->data, item->data_size); + break; + } + item++; + } + if (!item->key) lua_pushnil(L); + OSSL_PARAM_free(params); + } else { + lua_pushnil(L); + } + + 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 Lgenerate_p256_keypair(lua_State *L) { + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL); + + /* Generate key */ + if (EVP_PKEY_keygen_init(pctx) <= 0) goto err; + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1) <= 0) goto err; + if (EVP_PKEY_keygen(pctx, &pkey) <= 0) goto err; + EVP_PKEY_CTX_free(pctx); + + push_pkey(L, pkey, NID_X9_62_prime256v1, 1); + return 1; + +err: + if (pctx) EVP_PKEY_CTX_free(pctx); + lua_pushnil(L); + 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_ec_raw(lua_State *L) { + OSSL_PARAM_BLD *param_bld = NULL; + OSSL_PARAM *params = NULL; + EVP_PKEY_CTX *ctx = NULL; + EVP_PKEY *pkey = NULL; + + size_t pubkey_bytes; + const char* pubkey_data = luaL_checklstring(L, 1, &pubkey_bytes); + const char* curve = luaL_checkstring(L, 2); + + param_bld = OSSL_PARAM_BLD_new(); + if (!param_bld) goto err; + if (!OSSL_PARAM_BLD_push_utf8_string(param_bld, "group", curve, 0)) goto err; + if (!OSSL_PARAM_BLD_push_octet_string(param_bld, "pub", pubkey_data, pubkey_bytes)) goto err; + params = OSSL_PARAM_BLD_to_param(param_bld); + if (!params) goto err; + ctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL); + if (!ctx) goto err; + if (!EVP_PKEY_fromdata_init(ctx)) goto err; + if (EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_PUBLIC_KEY, params) <= 0) goto err; + + push_pkey(L, pkey, EVP_PKEY_id(pkey), 0); + + EVP_PKEY_CTX_free(ctx); + OSSL_PARAM_free(params); + OSSL_PARAM_BLD_free(param_bld); + + return 1; +err: + if (ctx) EVP_PKEY_CTX_free(ctx); + if (params) OSSL_PARAM_free(params); + if (param_bld) OSSL_PARAM_BLD_free(param_bld); + 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 }, + { "generate_p256_keypair", Lgenerate_p256_keypair }, + + { "import_private_pem", Limport_private_pem }, + { "import_public_pem", Limport_public_pem }, + { "import_public_ec_raw", Limport_public_ec_raw }, + + { "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 }, + { "public_raw", Lpkey_meth_public_raw }, + { "get_type", Lpkey_meth_get_type }, + { "derive", Lpkey_meth_derive }, + { 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 --git a/util-src/encodings.c b/util-src/encodings.c index 72264da8..ca2eb8b6 100644 --- a/util-src/encodings.c +++ b/util-src/encodings.c @@ -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 @@ static const luaL_Reg Reg_idna[] = { /***************** 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 @@ LUALIB_API int luaopen_util_encodings(lua_State *L) { lua_setfield(L, -2, "version"); return 1; } +LUALIB_API int luaopen_util_encodings(lua_State *L) { + return luaopen_prosody_util_encodings(L); +} diff --git a/util-src/hashes.c b/util-src/hashes.c index 8eefcd6b..3c2b16a7 100644 --- a/util-src/hashes.c +++ b/util-src/hashes.c @@ -28,13 +28,16 @@ typedef unsigned __int32 uint32_t; #include <openssl/md5.h> #include <openssl/hmac.h> #include <openssl/evp.h> +#include <openssl/kdf.h> +#include <openssl/err.h> -#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 @@ static void toHex(const unsigned char *in, int length, unsigned char *out) { } } -#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; \ -} - -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) - -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_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); -#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; \ -} - -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) + unsigned char hash[EVP_MAX_MD_SIZE], result[EVP_MAX_MD_SIZE * 2]; -static int Lpbkdf2_sha1(lua_State *L) { - unsigned char out[SHA_DIGEST_LENGTH]; + 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()); +} + +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()); +} + +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); + + 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()); +} + +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 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); +} - 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_sha256(), SHA256_DIGEST_LENGTH, out) == 0) { - return luaL_error(L, "PKCS5_PBKDF2_HMAC() failed"); +/* 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); + + 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 (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 @@ static const luaL_Reg Reg[] = { { "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 @@ LUALIB_API int luaopen_util_hashes(lua_State *L) { #endif return 1; } +LUALIB_API int luaopen_util_hashes(lua_State *L) { + return luaopen_prosody_util_hashes(L); +} diff --git a/util-src/managed_pointer.h b/util-src/managed_pointer.h new file mode 100644 index 00000000..213b5fd7 --- /dev/null +++ b/util-src/managed_pointer.h @@ -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 --git a/util-src/net.c b/util-src/net.c index d786e885..1a84c882 100644 --- a/util-src/net.c +++ b/util-src/net.c @@ -30,9 +30,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 @@ -192,10 +189,8 @@ static int lc_ntop(lua_State *L) { 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 @@ int luaopen_util_net(lua_State *L) { luaL_setfuncs(L, exports, 0); return 1; } +int luaopen_util_net(lua_State *L) { + return luaopen_prosody_util_net(L); +} diff --git a/util-src/poll.c b/util-src/poll.c index 81caa953..fc70bdb6 100644 --- a/util-src/poll.c +++ b/util-src/poll.c @@ -8,7 +8,6 @@ * */ -#include <unistd.h> #include <string.h> #include <errno.h> @@ -24,15 +23,18 @@ #endif #ifdef USE_EPOLL +#include <unistd.h> #include <sys/epoll.h> #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 <poll.h> -#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 @@ typedef struct Lpoll_state { #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 @@ static int Ladd(lua_State *L) { } } - 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 @@ static int Lwait(lua_State *L) { 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 @@ static int Lnew(lua_State *L) { 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 @@ static int Lnew(lua_State *L) { /* * 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 @@ int luaopen_util_poll(lua_State *L) { return 1; } +/* COMPAT */ +int luaopen_util_poll(lua_State *L) { + return luaopen_prosody_util_poll(L); +} + diff --git a/util-src/pposix.c b/util-src/pposix.c index a8e0720f..502e5326 100644 --- a/util-src/pposix.c +++ b/util-src/pposix.c @@ -13,7 +13,7 @@ * POSIX support functions for Lua */ -#define MODULE_VERSION "0.4.0" +#define MODULE_VERSION "0.4.1" #if defined(__linux__) @@ -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 @@ static int lc_mkdir(lua_State *L) { * 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 @@ static int lc_setrlimit(lua_State *L) { 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_setrlimit(lua_State *L) { 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 @@ static int lc_getrlimit(lua_State *L) { 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)) { @@ -671,6 +654,77 @@ static int lc_abort(lua_State *L) { return 0; } +const char *pipe_flag_names[] = { + "cloexec", + "direct", + "nonblock" +}; +const int pipe_flag_values[] = { + O_CLOEXEC, + O_DIRECT, + O_NONBLOCK +}; + + +static int lc_pipe(lua_State *L) { + int fds[2]; + int nflags = lua_gettop(L); + +#if defined(__linux__) + int flags=0; + for(int i = 1; i<=nflags; i++) { + int flag_index = luaL_checkoption(L, i, NULL, pipe_flag_names); + flags |= pipe_flag_values[flag_index]; + } + + if(pipe2(fds, flags) == -1) { +#else + if(nflags != 0) { + luaL_argerror(L, 1, "Flags are not supported on this platform"); + } + if(pipe(fds) == -1) { +#endif + luaL_pushfail(L); + lua_pushstring(L, strerror(errno)); + return 2; + } + + lua_pushinteger(L, fds[0]); + lua_pushinteger(L, fds[1]); + return 2; +} + +/* This helper function is adapted from Lua 5.3's liolib.c */ +static int stdio_fclose (lua_State *L) { + int res = -1; + luaL_Stream *p = ((luaL_Stream *)luaL_checkudata(L, 1, LUA_FILEHANDLE)); + if (p->f == NULL) { + return 0; + } + res = fclose(p->f); + p->f = NULL; + return luaL_fileresult(L, (res == 0), NULL); +} + +static int lc_fdopen(lua_State *L) { + int fd = luaL_checkinteger(L, 1); + const char *mode = luaL_checkstring(L, 2); + + luaL_Stream *file = (luaL_Stream *)lua_newuserdata(L, sizeof(luaL_Stream)); + file->closef = stdio_fclose; + file->f = fdopen(fd, mode); + + if (!file->f) { + luaL_pushfail(L); + lua_pushstring(L, strerror(errno)); + return 2; + } + + luaL_getmetatable(L, LUA_FILEHANDLE); + lua_setmetatable(L, -2); + return 1; +} + static int lc_uname(lua_State *L) { struct utsname uname_info; @@ -819,6 +873,41 @@ static int lc_atomic_append(lua_State *L) { 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 +917,8 @@ static int lc_isatty(lua_State *L) { /* 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 }, @@ -854,6 +941,9 @@ int luaopen_util_pposix(lua_State *L) { { "mkdir", lc_mkdir }, + { "pipe", lc_pipe }, + { "fdopen", lc_fdopen }, + { "setrlimit", lc_setrlimit }, { "getrlimit", lc_getrlimit }, @@ -866,6 +956,7 @@ int luaopen_util_pposix(lua_State *L) { #endif { "atomic_append", lc_atomic_append }, + { "remove_blocks", lc_remove_blocks }, { "isatty", lc_isatty }, @@ -888,3 +979,6 @@ int luaopen_util_pposix(lua_State *L) { return 1; } +int luaopen_util_pposix(lua_State *L) { + return luaopen_prosody_util_pposix(L); +} diff --git a/util-src/ringbuffer.c b/util-src/ringbuffer.c index 0f250c12..43846142 100644 --- a/util-src/ringbuffer.c +++ b/util-src/ringbuffer.c @@ -313,10 +313,8 @@ static int rb_new(lua_State *L) { 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 @@ int luaopen_util_ringbuffer(lua_State *L) { lua_setfield(L, -2, "new"); return 1; } + +int luaopen_util_ringbuffer(lua_State *L) { + return luaopen_prosody_util_ringbuffer(L); +} diff --git a/util-src/signal.c b/util-src/signal.c index 1a398fa0..76d25d6f 100644 --- a/util-src/signal.c +++ b/util-src/signal.c @@ -32,13 +32,14 @@ #include <signal.h> #include <stdlib.h> +#ifdef __linux__ +#include <unistd.h> +#include <sys/signalfd.h> +#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,21 +372,105 @@ static int l_kill(lua_State *L) { #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); -#endif 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 + /* add the library */ lua_newtable(L); luaL_setfuncs(L, lsignal_lib, 0); @@ -413,3 +498,6 @@ int luaopen_util_signal(lua_State *L) { return 1; } +int luaopen_util_signal(lua_State *L) { + return luaopen_prosody_util_signal(L); +} diff --git a/util-src/strbitop.c b/util-src/strbitop.c index 89fce661..2f6bf6e6 100644 --- a/util-src/strbitop.c +++ b/util-src/strbitop.c @@ -8,13 +8,12 @@ #include <lua.h> #include <lauxlib.h> -#if (LUA_VERSION_NUM == 501) -#define luaL_setfuncs(L, R, N) luaL_register(L, NULL, R) -#endif +#include <sys/param.h> +#include <limits.h> /* 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 @@ int strop_and(lua_State *L) { 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 @@ int strop_or(lua_State *L) { 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 @@ int strop_xor(lua_State *L) { 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<min_len; i++) { + if(str_a[i] != str_b[i]) { + lua_pushinteger(L, i*8 + (clz(str_a[i] ^ str_b[i]))); + return 1; + } + } + + lua_pushinteger(L, i*8); + return 1; +} + +LUA_API int luaopen_prosody_util_strbitop(lua_State *L) { luaL_Reg exports[] = { { "sand", strop_and }, { "sor", strop_or }, { "sxor", strop_xor }, + { "common_prefix_bits", strop_common_prefix_bits }, { NULL, NULL } }; @@ -89,3 +123,7 @@ LUA_API int luaopen_util_strbitop(lua_State *L) { luaL_setfuncs(L, exports, 0); return 1; } + +LUA_API int luaopen_util_strbitop(lua_State *L) { + return luaopen_prosody_util_strbitop(L); +} diff --git a/util-src/struct.c b/util-src/struct.c index e80df4e6..a779f53e 100644 --- a/util-src/struct.c +++ b/util-src/struct.c @@ -36,12 +36,6 @@ #include "lauxlib.h" -#if (LUA_VERSION_NUM >= 502) - -#define luaL_register(L,n,f) luaL_newlib(L,f) - -#endif - /* basic integer type */ #if !defined(STRUCT_INT) @@ -140,7 +134,7 @@ static int gettoalign (size_t len, Header *h, int opt, size_t size) { /* -** 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,11 +385,15 @@ static const struct luaL_Reg thislib[] = { LUALIB_API int luaopen_util_struct (lua_State *L); -LUALIB_API int luaopen_util_struct (lua_State *L) { - luaL_register(L, "struct", thislib); +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) { + return luaopen_prosody_util_struct(L); +} + /****************************************************************************** * Copyright (C) 2010-2018 Lua.org, PUC-Rio. All rights reserved. diff --git a/util-src/table.c b/util-src/table.c index 9a9553fc..7e01c072 100644 --- a/util-src/table.c +++ b/util-src/table.c @@ -1,11 +1,17 @@ #include <lua.h> #include <lauxlib.h> +#ifndef LUA_MAXINTEGER +#include <stdint.h> +#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 @@ static int Lpack(lua_State *L) { 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 --git a/util-src/time.c b/util-src/time.c index afef3df5..e5e72a09 100644 --- a/util-src/time.c +++ b/util-src/time.c @@ -23,7 +23,7 @@ static int lc_time_monotonic(lua_State *L) { 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 @@ int luaopen_util_time(lua_State *L) { } return 1; } +int luaopen_util_time(lua_State *L) { + return luaopen_prosody_util_time(L); +} diff --git a/util-src/windows.c b/util-src/windows.c index 57af79d5..2adb85f5 100644 --- a/util-src/windows.c +++ b/util-src/windows.c @@ -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 @@ static const luaL_Reg Reg[] = { }; 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 --git a/util/adminstream.lua b/util/adminstream.lua index 4075aa05..a29222cf 100644 --- a/util/adminstream.lua +++ b/util/adminstream.lua @@ -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 @@ local function new_connection(socket_path, listeners) -- 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 --git a/util/argparse.lua b/util/argparse.lua index 9ece050a..7a55cb0b 100644 --- a/util/argparse.lua +++ b/util/argparse.lua @@ -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 @@ local function parse(arg, config) 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 @@ local function parse(arg, config) param_k, param_v = param, true; end end + param_k = param_k:gsub("%-", "_"); + end + 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 - parsed_opts[param_k] = param_v; end for i = 1, #arg do parsed_opts[i] = arg[i]; diff --git a/util/array.lua b/util/array.lua index 39a97e7f..074e9bc7 100644 --- a/util/array.lua +++ b/util/array.lua @@ -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_methods = {}; 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 @@ function array_base.slice(outa, ina, i, j) 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 @@ function array_methods:shuffle() 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 --git a/util/async.lua b/util/async.lua index 2830238f..9a3485e9 100644 --- a/util/async.lua +++ b/util/async.lua @@ -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 @@ end 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 @@ end 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 @@ function runner_mt:run(input) -- 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 @@ function runner_mt:run(input) 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 @@ function runner_mt:run(input) 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 @@ return { 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 --git a/util/bit53.lua b/util/bit53.lua index b5c473a3..42f17ce8 100644 --- a/util/bit53.lua +++ b/util/bit53.lua @@ -27,6 +27,9 @@ return { 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 --git a/util/bitcompat.lua b/util/bitcompat.lua index 454181af..43fd0f6e 100644 --- a/util/bitcompat.lua +++ b/util/bitcompat.lua @@ -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 --git a/util/cache.lua b/util/cache.lua index cd1b4544..e1873cc8 100644 --- a/util/cache.lua +++ b/util/cache.lua @@ -54,12 +54,17 @@ function cache_methods:set(k, v) 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 @@ function cache_methods:resize(new_size) 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 --git a/util/caps.lua b/util/caps.lua index de492edb..08743393 100644 --- a/util/caps.lua +++ b/util/caps.lua @@ -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 --git a/util/dataforms.lua b/util/dataforms.lua index 66733895..deb570cb 100644 --- a/util/dataforms.lua +++ b/util/dataforms.lua @@ -12,9 +12,9 @@ local type, next = type, next; 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 --git a/util/datamanager.lua b/util/datamanager.lua index c57f4a0e..8192d12a 100644 --- a/util/datamanager.lua +++ b/util/datamanager.lua @@ -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 @@ local function append(username, host, datastore, ext, data) 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 @@ local function append(username, host, datastore, ext, data) 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 @@ local function list_append(username, host, datastore, data) 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 @@ local function list_store(username, host, datastore, data) 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 @@ local function list_store(username, host, datastore, data) 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 @@ local function list_load(username, host, datastore) return items; end +local function list_open(username, host, datastore) + if not index_magic then + log("debug", "Falling back from lazy loading 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 function purge(username, host) 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 @@ return { purge = purge; path_decode = decode; path_encode = encode; + + build_list_index = build_list_index; + list_open = list_open; + list_shift = list_shift; }; diff --git a/util/datamapper.lua b/util/datamapper.lua index 2378314c..03abc7ad 100644 --- a/util/datamapper.lua +++ b/util/datamapper.lua @@ -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 --git a/util/datetime.lua b/util/datetime.lua index 2d27ece4..6df146f4 100644 --- a/util/datetime.lua +++ b/util/datetime.lua @@ -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 @@ local function parse(s) 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 --git a/util/dbuffer.lua b/util/dbuffer.lua index 3ad5fdfe..4c00eabc 100644 --- a/util/dbuffer.lua +++ b/util/dbuffer.lua @@ -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 @@ function dbuffer_methods:read_until(char) 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 --git a/util/debug.lua b/util/debug.lua index 4c924d40..7a8312a9 100644 --- a/util/debug.lua +++ b/util/debug.lua @@ -12,7 +12,7 @@ local censored_names = { }; local optimal_line_length = 65; -local termcolours = require "util.termcolours"; +local termcolours = require "prosody.util.termcolours"; local getstring = termcolours.getstring; local styles; do diff --git a/util/dependencies.lua b/util/dependencies.lua index d7836404..30b53970 100644 --- a/util/dependencies.lua +++ b/util/dependencies.lua @@ -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 @@ local function missingdep(name, sources, msg, err) -- luacheck: ignore err 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 @@ local function missingdep(name, sources, msg, err) -- luacheck: ignore err 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 @@ local function check_dependencies() }, 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 @@ local function check_dependencies() { "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 @@ local function check_dependencies() 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 @@ local function log_warnings() 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 --git a/util/dns.lua b/util/dns.lua index 3b58e03e..f113f97e 100644 --- a/util/dns.lua +++ b/util/dns.lua @@ -12,9 +12,9 @@ local s_byte = string.byte; 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 --git a/util/dnsregistry.lua b/util/dnsregistry.lua index 635b7e3a..b65debe0 100644 --- a/util/dnsregistry.lua +++ b/util/dnsregistry.lua @@ -1,5 +1,5 @@ -- Source: https://www.iana.org/assignments/dns-parameters/dns-parameters.xml --- Generated on 2022-02-02 +-- Generated on 2024-10-26 return { classes = { ["IN"] = 1; [1] = "IN"; @@ -61,7 +61,6 @@ return { ["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"; @@ -80,6 +79,7 @@ return { ["LP"] = 107; [107] = "LP"; ["EUI48"] = 108; [108] = "EUI48"; ["EUI64"] = 109; [109] = "EUI64"; + ["NXNAME"] = 128; [128] = "NXNAME"; ["TKEY"] = 249; [249] = "TKEY"; ["TSIG"] = 250; [250] = "TSIG"; ["IXFR"] = 251; [251] = "IXFR"; @@ -92,6 +92,10 @@ return { ["AVC"] = 258; [258] = "AVC"; ["DOA"] = 259; [259] = "DOA"; ["AMTRELAY"] = 260; [260] = "AMTRELAY"; + ["RESINFO"] = 261; [261] = "RESINFO"; + ["WALLET"] = 262; [262] = "WALLET"; + ["CLA"] = 263; [263] = "CLA"; + ["IPN"] = 264; [264] = "IPN"; ["TA"] = 32768; [32768] = "TA"; ["DLV"] = 32769; [32769] = "DLV"; }; diff --git a/util/envload.lua b/util/envload.lua index 6182a1f9..cf45b702 100644 --- a/util/envload.lua +++ b/util/envload.lua @@ -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 - - 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 +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(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 --git a/util/error.lua b/util/error.lua index b83f81e5..64c742ae 100644 --- a/util/error.lua +++ b/util/error.lua @@ -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 @@ local function configure(opt) 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 --git a/util/format.lua b/util/format.lua index d709aada..f95f0575 100644 --- a/util/format.lua +++ b/util/format.lua @@ -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 @@ local control_symbols = { ["\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 @@ local function format(formatstring, ...) 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 --git a/util/fsm.lua b/util/fsm.lua new file mode 100644 index 00000000..0afc2d16 --- /dev/null +++ b/util/fsm.lua @@ -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 --git a/util/gc.lua b/util/gc.lua index f46e4346..7fcf7546 100644 --- a/util/gc.lua +++ b/util/gc.lua @@ -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 --git a/util/hashring.lua b/util/hashring.lua index d4555669..a1debb53 100644 --- a/util/hashring.lua +++ b/util/hashring.lua @@ -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 @@ local function new(num_replicas, hash_function) 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 @@ end 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 @@ end 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 --git a/util/helpers.lua b/util/helpers.lua index 139b62ec..da680cae 100644 --- a/util/helpers.lua +++ b/util/helpers.lua @@ -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 --git a/util/hmac.lua b/util/hmac.lua index 4cad17cc..c782da3c 100644 --- a/util/hmac.lua +++ b/util/hmac.lua @@ -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 --git a/util/http.lua b/util/http.lua index 3852f91c..b21bf798 100644 --- a/util/http.lua +++ b/util/http.lua @@ -69,9 +69,42 @@ local function normalize_path(path, is_dir) 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 --git a/util/human/io.lua b/util/human/io.lua index 7d7dea97..d6112b3b 100644 --- a/util/human/io.lua +++ b/util/human/io.lua @@ -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 @@ end; 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 @@ local function getline() 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 @@ if utf8.len and utf8.offset then 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 @@ local function new_table(col_specs, max_width) -- 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 function new_table(col_specs, max_width) 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 @@ local function new_table(col_specs, max_width) 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 @@ return { 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 --git a/util/human/units.lua b/util/human/units.lua index af233e98..329c8518 100644 --- a/util/human/units.lua +++ b/util/human/units.lua @@ -4,15 +4,7 @@ local math_floor = math.floor; 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 --git a/util/id.lua b/util/id.lua index ff4e919d..70b0cecc 100644 --- a/util/id.lua +++ b/util/id.lua @@ -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 --git a/util/import.lua b/util/import.lua index 1007bc0a..0892e9b1 100644 --- a/util/import.lua +++ b/util/import.lua @@ -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 --git a/util/ip.lua b/util/ip.lua index 4b450934..d820e72d 100644 --- a/util/ip.lua +++ b/util/ip.lua @@ -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 @@ ip_mt.__eq = function (ipA, ipB) 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 @@ function ip_methods:normal() 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 @@ function match(ipA, ipB, bits) 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 @@ return { commonPrefixLength = commonPrefixLength, parse_cidr = parse_cidr, match = match, + is_ip = is_ip; + truncate = truncate; }; diff --git a/util/iterators.lua b/util/iterators.lua index c03c2fd6..eb4c54af 100644 --- a/util/iterators.lua +++ b/util/iterators.lua @@ -12,8 +12,8 @@ local it = {}; 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 @@ function join_methods:prepend(f, s, var) 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 --git a/util/jid.lua b/util/jid.lua index 694a6b1f..2c3436ca 100644 --- a/util/jid.lua +++ b/util/jid.lua @@ -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 @@ local function split(jid) 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 @@ local function compare(jid, acl) -- 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 @@ local function resource(jid) 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 --git a/util/json.lua b/util/json.lua index e6704b7e..369b9762 100644 --- a/util/json.lua +++ b/util/json.lua @@ -10,12 +10,12 @@ local type = type; 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 --git a/util/jsonpointer.lua b/util/jsonpointer.lua index 9b871ae7..f1c354a4 100644 --- a/util/jsonpointer.lua +++ b/util/jsonpointer.lua @@ -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 --git a/util/jsonschema.lua b/util/jsonschema.lua index eafa8b7c..d7a2f9c9 100644 --- a/util/jsonschema.lua +++ b/util/jsonschema.lua @@ -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"; -end; -local json = require("util.json") -local null = json.null; +if not math.type then + require("prosody.util.mathcompat") +end -local pointer = require("util.jsonpointer") +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_type_name = json.json_type_name +local json = require("prosody.util.json") +local null = json.null; -local schema_t = {} +local pointer = require("prosody.util.jsonpointer") local json_schema_object = { xml_t = {} } @@ -22,7 +25,7 @@ local function simple_validate(schema, data) 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 @@ local function simple_validate(schema, data) 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 @@ function complex_validate(schema, data, root) 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 @@ function complex_validate(schema, data, root) 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 @@ function complex_validate(schema, data, root) 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 utf8_len(data) < schema.minLength then + table.insert(errs, mkerr(sloc .. "/maxLength", iloc, "string too short")) + return false, errs end - if schema.minLength and #data < schema.minLength then - return false + 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 @@ function complex_validate(schema, data, root) 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 --git a/util/jwt.lua b/util/jwt.lua index bf106dfa..997f0068 100644 --- a/util/jwt.lua +++ b/util/jwt.lua @@ -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 @@ local function unb64url(data) 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 function verify(key, blob) 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 +-- 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 { - sign = sign; - verify = verify; + 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 --git a/util/logger.lua b/util/logger.lua index 20a5cef2..ad6921a1 100644 --- a/util/logger.lua +++ b/util/logger.lua @@ -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 @@ local function add_level_sink(level, sink_function) 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 @@ return { add_level_sink = add_level_sink; add_simple_sink = add_simple_sink; new = make_logger; + remove_sink = remove_sink; }; diff --git a/util/mathcompat.lua b/util/mathcompat.lua new file mode 100644 index 00000000..e8acb261 --- /dev/null +++ b/util/mathcompat.lua @@ -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 --git a/util/multitable.lua b/util/multitable.lua index 4f2cd972..0c292b45 100644 --- a/util/multitable.lua +++ b/util/multitable.lua @@ -9,7 +9,7 @@ local select = select; local t_insert = table.insert; local pairs, next, type = pairs, next, type; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local unpack = table.unpack; local _ENV = nil; -- luacheck: std none diff --git a/util/openmetrics.lua b/util/openmetrics.lua index c18e63e9..cf9f5d24 100644 --- a/util/openmetrics.lua +++ b/util/openmetrics.lua @@ -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 @@ defined in the I-D linked above. -- 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 @@ local t_pack, t_unpack = require "util.table".pack, table.unpack or unpack; --lu -- `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 @@ end -- 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 @@ local function compose_name(name, unit) 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 --git a/util/openssl.lua b/util/openssl.lua index 32b5aea7..422bc494 100644 --- a/util/openssl.lua +++ b/util/openssl.lua @@ -5,7 +5,7 @@ local s_format = string.format; 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 @@ do -- Lua to shell calls. 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 --git a/util/paseto.lua b/util/paseto.lua new file mode 100644 index 00000000..5a9acafb --- /dev/null +++ b/util/paseto.lua @@ -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("<I8", bit.band(n, 0x7F)); +end + +local function pae(parts) + if type(parts) ~= "table" then + error("bad argument #1 to 'pae' (table expected, got "..type(parts)..")"); + end + local o = { le64(#parts) }; + for _, part in ipairs(parts) do + table.insert(o, le64(#part)..part); + end + return table.concat(o); +end + +function v4_public.sign(m, sk, f, i) + if type(m) ~= "table" then + return nil, "PASETO payloads must be a table"; + end + m = json.encode(m); + local h = "v4.public."; + local m2 = pae({ h, m, f or "", i or "" }); + local sig = crypto.ed25519_sign(sk, m2); + if not f or f == "" then + return h..b64url(m..sig); + else + return h..b64url(m..sig).."."..b64url(f); + end +end + +function v4_public.verify(tok, pk, expected_f, i) + local h, sm, f = tok:match("^(v4%.public%.)([^%.]+)%.?(.*)$"); + if not h then + return nil, "invalid-token-format"; + end + f = f and unb64url(f) or nil; + if expected_f then + if not f or not secure_equals(expected_f, f) then + return nil, "invalid-footer"; + end + end + local raw_sm = unb64url(sm); + if not raw_sm or #raw_sm <= 64 then + return nil, "invalid-token-format"; + end + local s, m = raw_sm:sub(-64), raw_sm:sub(1, -65); + local m2 = pae({ h, m, f or "", i or "" }); + local ok = crypto.ed25519_verify(pk, m2, s); + if not ok then + return nil, "invalid-token"; + end + local payload, err = json.decode(m); + if err ~= nil or type(payload) ~= "table" then + return nil, "json-decode-error"; + end + return payload; +end + +v4_public.import_private_key = crypto.import_private_pem; +v4_public.import_public_key = crypto.import_public_pem; +function v4_public.new_keypair() + return crypto.generate_ed25519_keypair(); +end + +function v4_public.init(private_key_pem, public_key_pem, options) + local sign, verify = v4_public.sign, v4_public.verify; + local public_key = public_key_pem and v4_public.import_public_key(public_key_pem); + local private_key = private_key_pem and v4_public.import_private_key(private_key_pem); + local default_footer = options and options.default_footer; + local default_assertion = options and options.default_implicit_assertion; + return private_key and function (token, token_footer, token_assertion) + return sign(token, private_key, token_footer or default_footer, token_assertion or default_assertion); + end, public_key and function (token, expected_footer, token_assertion) + return verify(token, public_key, expected_footer or default_footer, token_assertion or default_assertion); + end; +end + +function v4_public.new_signer(private_key_pem, options) + return (v4_public.init(private_key_pem, nil, options)); +end + +function v4_public.new_verifier(public_key_pem, options) + return (select(2, v4_public.init(nil, public_key_pem, options))); +end + +local v3_local = { _key_mt = {} }; + +local function v3_local_derive_keys(k, n) + local tmp = hashes.hkdf_hmac_sha384(48, k, nil, "paseto-encryption-key"..n); + local Ek = tmp:sub(1, 32); + local n2 = tmp:sub(33); + local Ak = hashes.hkdf_hmac_sha384(48, k, nil, "paseto-auth-key-for-aead"..n); + return Ek, Ak, n2; +end + +function v3_local.encrypt(m, k, f, i) + assert(#k == 32) + if type(m) ~= "table" then + return nil, "PASETO payloads must be a table"; + end + m = json.encode(m); + local h = "v3.local."; + local n = rand.bytes(32); + local Ek, Ak, n2 = v3_local_derive_keys(k, n); + + local c = crypto.aes_256_ctr_encrypt(Ek, n2, m); + local m2 = pae({ h, n, c, f or "", i or "" }); + local t = hashes.hmac_sha384(Ak, m2); + + if not f or f == "" then + return h..b64url(n..c..t); + else + return h..b64url(n..c..t).."."..b64url(f); + end +end + +function v3_local.decrypt(tok, k, expected_f, i) + assert(#k == 32) + + local h, sm, f = tok:match("^(v3%.local%.)([^%.]+)%.?(.*)$"); + if not h then + return nil, "invalid-token-format"; + end + f = f and unb64url(f) or nil; + if expected_f then + if not f or not secure_equals(expected_f, f) then + return nil, "invalid-footer"; + end + end + local m = unb64url(sm); + if not m or #m <= 80 then + return nil, "invalid-token-format"; + end + local n, c, t = m:sub(1, 32), m:sub(33, -49), m:sub(-48); + local Ek, Ak, n2 = v3_local_derive_keys(k, n); + local preAuth = pae({ h, n, c, f or "", i or "" }); + local t2 = hashes.hmac_sha384(Ak, preAuth); + if not secure_equals(t, t2) then + return nil, "invalid-token"; + end + local m2 = crypto.aes_256_ctr_decrypt(Ek, n2, c); + if not m2 then + return nil, "invalid-token"; + end + + local payload, err = json.decode(m2); + if err ~= nil or type(payload) ~= "table" then + return nil, "json-decode-error"; + end + return payload; +end + +function v3_local.new_key() + return "secret-token:paseto.v3.local:"..hex.encode(rand.bytes(32)); +end + +function v3_local.init(key, options) + local encoded_key = key:match("^secret%-token:paseto%.v3%.local:(%x+)$"); + if not encoded_key or #encoded_key ~= 64 then + return error("invalid key for v3.local"); + end + local raw_key = hex.decode(encoded_key); + local default_footer = options and options.default_footer; + local default_assertion = options and options.default_implicit_assertion; + return function (token, token_footer, token_assertion) + return v3_local.encrypt(token, raw_key, token_footer or default_footer, token_assertion or default_assertion); + end, function (token, token_footer, token_assertion) + return v3_local.decrypt(token, raw_key, token_footer or default_footer, token_assertion or default_assertion); + end; +end + +function v3_local.new_signer(key, options) + return (v3_local.init(key, options)); +end + +function v3_local.new_verifier(key, options) + return (select(2, v3_local.init(key, options))); +end + +return { + pae = pae; + v3_local = v3_local; + v4_public = v4_public; +}; diff --git a/util/pluginloader.lua b/util/pluginloader.lua index f2ccb4cb..634bd6f8 100644 --- a/util/pluginloader.lua +++ b/util/pluginloader.lua @@ -17,7 +17,7 @@ for path in (CFG_PLUGINDIR or "./plugins/"):gsub("[/\\]", dir_sep):gmatch("[^".. end local io_open = io.open; -local envload = require "util.envload".envload; +local envload = require "prosody.util.envload".envload; local pluginloader_methods = {}; local pluginloader_mt = { __index = pluginloader_methods }; diff --git a/util/promise.lua b/util/promise.lua index c4e166ed..90780626 100644 --- a/util/promise.lua +++ b/util/promise.lua @@ -1,8 +1,8 @@ local promise_methods = {}; local promise_mt = { __name = "promise", __index = promise_methods }; -local xpcall = require "util.xpcall".xpcall; -local unpack = table.unpack or unpack; --luacheck: ignore 113 +local xpcall = require "prosody.util.xpcall".xpcall; +local unpack = table.unpack; function promise_mt:__tostring() return "promise (" .. (self._state or "invalid") .. ")"; @@ -57,10 +57,7 @@ local function promise_settle(promise, new_state, new_next, cbs, value) end local function new_resolve_functions(p) - local resolved = false; local function _resolve(v) - if resolved then return; end - resolved = true; if is_promise(v) then v:next(new_resolve_functions(p)); elseif promise_settle(p, "fulfilled", next_fulfilled, p._pending_on_fulfilled, v) then @@ -69,8 +66,6 @@ local function new_resolve_functions(p) end local function _reject(e) - if resolved then return; end - resolved = true; if promise_settle(p, "rejected", next_rejected, p._pending_on_rejected, e) then p.reason = e; end diff --git a/util/prosodyctl.lua b/util/prosodyctl.lua index 4d49cd16..9cb4b4dd 100644 --- a/util/prosodyctl.lua +++ b/util/prosodyctl.lua @@ -7,17 +7,21 @@ -- -local config = require "core.configmanager"; -local encodings = require "util.encodings"; +local config = require "prosody.core.configmanager"; +local encodings = require "prosody.util.encodings"; local stringprep = encodings.stringprep; -local storagemanager = require "core.storagemanager"; -local usermanager = require "core.usermanager"; -local interpolation = require "util.interpolation"; -local signal = require "util.signal"; -local set = require "util.set"; +local storagemanager = require "prosody.core.storagemanager"; +local usermanager = require "prosody.core.usermanager"; +local interpolation = require "prosody.util.interpolation"; +local signal = require "prosody.util.signal"; +local set = require "prosody.util.set"; +local path = require"prosody.util.paths"; local lfs = require "lfs"; local type = type; +local have_socket_unix, socket_unix = pcall(require, "socket.unix"); +have_socket_unix = have_socket_unix and type(socket_unix) == "table"; -- was a function in older LuaSocket + local nodeprep, nameprep = stringprep.nodeprep, stringprep.nameprep; local io, os = io, os; @@ -42,7 +46,7 @@ local error_messages = setmetatable({ }, { __index = function (_,k) return "Error: "..(tostring(k):gsub("%-", " "):gsub("^.", string.upper)); end }); -- UI helpers -local show_message = require "util.human.io".printf; +local show_message = require "prosody.util.human.io".printf; local function show_usage(usage, desc) print("Usage: ".._G.arg[0].." "..usage); @@ -177,11 +181,31 @@ local function start(source_dir, lua) if ret then return false, "already-running"; end + local notify_socket; + if have_socket_unix then + local notify_path = path.join(prosody.paths.data, "notify.sock"); + os.remove(notify_path); + lua = string.format("NOTIFY_SOCKET=%q %s", notify_path, lua); + notify_socket = socket_unix.dgram(); + local ok = notify_socket:setsockname(notify_path); + if not ok then return false, "notify-failed"; end + end if not source_dir then os.execute(lua .. "./prosody -D"); else os.execute(lua .. source_dir.."/../../bin/prosody -D"); end + + if notify_socket then + for i = 1, 5 do + notify_socket:settimeout(i); + if notify_socket:receivefrom() == "READY=1" then + return true; + end + end + return false, "not-ready"; + end + return true; end @@ -224,8 +248,7 @@ local function call_luarocks(operation, mod, server) local ok, _, code = os.execute(render_cli("luarocks --lua-version={luav} {op} --tree={dir} {server&--server={server}} {mod?}", { dir = dir; op = operation; mod = mod; server = server; luav = _VERSION:match("5%.%d"); })); - if type(ok) == "number" then code = ok; end - return code; + return ok and code; end return { diff --git a/util/prosodyctl/cert.lua b/util/prosodyctl/cert.lua index abd2e1d6..70c09443 100644 --- a/util/prosodyctl/cert.lua +++ b/util/prosodyctl/cert.lua @@ -1,8 +1,8 @@ local lfs = require "lfs"; -local pctl = require "util.prosodyctl"; -local hi = require "util.human.io"; -local configmanager = require "core.configmanager"; +local pctl = require "prosody.util.prosodyctl"; +local hi = require "prosody.util.human.io"; +local configmanager = require "prosody.core.configmanager"; local openssl; @@ -24,7 +24,7 @@ local function use_existing(filename) end end -local have_pposix, pposix = pcall(require, "util.pposix"); +local have_pposix, pposix = pcall(require, "prosody.util.pposix"); local cert_basedir = prosody.paths.data == "." and "./certs" or prosody.paths.data; if have_pposix and pposix.getuid() == 0 then -- FIXME should be enough to check if this directory is writable @@ -179,7 +179,7 @@ local function copy(from, to, umask, owner, group) os.execute(("chown -c --reference=%s %s"):format(sh_esc(cert_basedir), sh_esc(to))); elseif owner and group then local ok = os.execute(("chown %s:%s %s"):format(sh_esc(owner), sh_esc(group), sh_esc(to))); - assert(ok == true or ok == 0, "Failed to change ownership of "..to); + assert(ok, "Failed to change ownership of "..to); end if old_umask then pposix.umask(old_umask); end return true; @@ -219,7 +219,7 @@ function cert_commands.import(arg) owner = configmanager.get("*", "prosody_user") or "prosody"; group = configmanager.get("*", "prosody_group") or owner; end - local cm = require "core.certmanager"; + local cm = require "prosody.core.certmanager"; local files_by_name = {} for _, dir in ipairs(arg) do cm.index_certs(dir, files_by_name); @@ -271,7 +271,7 @@ end local function cert(arg) if #arg >= 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 @@ local function cert(arg) 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 --git a/util/prosodyctl/check.lua b/util/prosodyctl/check.lua index 43406f0c..ac8cc9c1 100644 --- a/util/prosodyctl/check.lua +++ b/util/prosodyctl/check.lua @@ -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 @@ local function check_ojn(check_type, target_host) 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 @@ local function check_probe(base_url, probe_module, target) 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"; local result = { warnings = {} }; @@ -170,7 +170,7 @@ local function check_turn_service(turn_service, ping_service) 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()); @@ -319,18 +319,15 @@ local function check(arg) 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 @@ -345,7 +342,7 @@ local function check(arg) print"" end end - if not what or what == "config" then + function checks.config() print("Checking config..."); if what == "config" then @@ -520,9 +517,9 @@ local function check(arg) 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 @@ -750,7 +747,7 @@ local function check(arg) -- 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 @@ -801,14 +798,14 @@ local function check(arg) 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}); @@ -871,7 +868,7 @@ local function check(arg) 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 @@ -1038,9 +1035,6 @@ local function check(arg) 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 @@ -1176,11 +1170,14 @@ local function check(arg) 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; @@ -1193,13 +1190,14 @@ local function check(arg) 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 @@ -1234,17 +1232,39 @@ local function check(arg) 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 @@ -1257,7 +1277,7 @@ local function check(arg) 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."); @@ -1349,7 +1369,7 @@ local function check(arg) 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 = {}; @@ -1424,6 +1444,26 @@ local function check(arg) 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 --git a/util/prosodyctl/shell.lua b/util/prosodyctl/shell.lua index 8cf7df69..05f81f15 100644 --- a/util/prosodyctl/shell.lua +++ b/util/prosodyctl/shell.lua @@ -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 @@ local function read_line(prompt_string) 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 @@ end 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 @@ local function start(arg) --luacheck: ignore 212/arg 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]:match("%%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 @@ local function start(arg) --luacheck: ignore 212/arg 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 --git a/util/pubsub.lua b/util/pubsub.lua index acb34db9..d6779736 100644 --- a/util/pubsub.lua +++ b/util/pubsub.lua @@ -1,6 +1,5 @@ -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 service_mt = {}; @@ -12,6 +11,7 @@ local default_config = { itemcheck = function () return true; end; get_affiliation = function () end; normalize_jid = function (jid) return jid; end; + metadata_subset = {}; capabilities = { outcast = { create = false; @@ -46,6 +46,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = false; + get_metadata = true; subscribe_other = false; unsubscribe_other = false; @@ -68,6 +69,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = true; + get_metadata = true; subscribe_other = false; unsubscribe_other = false; @@ -91,6 +93,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = true; + get_metadata = true; subscribe_other = false; unsubscribe_other = false; @@ -116,6 +119,7 @@ local default_config = { get_subscription = true; get_subscriptions = true; get_items = true; + get_metadata = true; subscribe_other = true; @@ -263,7 +267,7 @@ function service:get_default_affiliation(node, actor) --> affiliation if self.config.access_models then local check = self.config.access_models[access_model]; if check then - local aff = check(actor); + local aff = check(actor, node_obj); if aff then return aff; end @@ -562,11 +566,7 @@ function service:publish(node, actor, id, item, requested_config) --> ok, err -- Check that node has the requested config before we publish local ok, field = check_preconditions(node_obj.config, requested_config); if not ok then - local err = errors.new({ - type = "cancel", condition = "conflict", text = "Field does not match: "..field; - }); - err.pubsub_condition = "precondition-not-met"; - return false, err; + return false, "precondition-not-met", { field = field }; end end if not self.config.itemcheck(item) then @@ -877,6 +877,20 @@ function service:get_node_config(node, actor) --> (true, config) or (false, err) return true, config_table; end +function service:get_node_metadata(node, actor) + if not self:may(node, actor, "get_metadata") then + return false, "forbidden"; + end + + local ok, config = self:get_node_config(node, true); + if not ok then return ok, config; end + local meta = {}; + for _, k in ipairs(self.config.metadata_subset) do + meta[k] = config[k]; + end + return true, meta; +end + return { new = new; }; diff --git a/util/queue.lua b/util/queue.lua index c94c62ae..8cff944a 100644 --- a/util/queue.lua +++ b/util/queue.lua @@ -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 --git a/util/random.lua b/util/random.lua index 3305172f..a3adb605 100644 --- a/util/random.lua +++ b/util/random.lua @@ -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 --git a/util/rfc6724.lua b/util/rfc6724.lua deleted file mode 100644 index 81f78d55..00000000 --- a/util/rfc6724.lua +++ /dev/null @@ -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 --git a/util/roles.lua b/util/roles.lua new file mode 100644 index 00000000..e7f22c12 --- /dev/null +++ b/util/roles.lua @@ -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 --git a/util/rsm.lua b/util/rsm.lua index e6060af8..fc6a048d 100644 --- a/util/rsm.lua +++ b/util/rsm.lua @@ -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 --git a/util/sasl.lua b/util/sasl.lua index 528743d1..c3c22a1c 100644 --- a/util/sasl.lua +++ b/util/sasl.lua @@ -47,7 +47,7 @@ local registered_mechanisms = {}; 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 @@ function method:process(message) 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 --git a/util/sasl/anonymous.lua b/util/sasl/anonymous.lua index de98a5e2..be2c20d4 100644 --- a/util/sasl/anonymous.lua +++ b/util/sasl/anonymous.lua @@ -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 function anonymous(self, message) -- luacheck: ignore 212/message 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 --git a/util/sasl/external.lua b/util/sasl/external.lua index ce50743e..c3a3beb8 100644 --- a/util/sasl/external.lua +++ b/util/sasl/external.lua @@ -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 --git a/util/sasl/oauthbearer.lua b/util/sasl/oauthbearer.lua new file mode 100644 index 00000000..0a2fe9dd --- /dev/null +++ b/util/sasl/oauthbearer.lua @@ -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 --git a/util/sasl/plain.lua b/util/sasl/plain.lua index 43a66c5b..da867fb1 100644 --- a/util/sasl/plain.lua +++ b/util/sasl/plain.lua @@ -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 function plain(self, message) 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 --git a/util/sasl/scram.lua b/util/sasl/scram.lua index 37abf4a4..ad279999 100644 --- a/util/sasl/scram.lua +++ b/util/sasl/scram.lua @@ -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 function scram_gen(hash_name, H_f, HMAC_f, get_auth_db, expect_cb) 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 @@ local function scram_gen(hash_name, H_f, HMAC_f, get_auth_db, expect_cb) 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 @@ local function scram_gen(hash_name, H_f, HMAC_f, get_auth_db, expect_cb) -- 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 @@ local function scram_gen(hash_name, H_f, HMAC_f, get_auth_db, expect_cb) 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 @@ local function init(registerMechanism) -- 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 --git a/util/serialization.lua b/util/serialization.lua index d310a3e8..ee4751e2 100644 --- a/util/serialization.lua +++ b/util/serialization.lua @@ -16,15 +16,17 @@ local s_char = string.char; 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 @@ local function new(opt) 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 --git a/util/session.lua b/util/session.lua index 25b22faf..2d3cbd54 100644 --- a/util/session.lua +++ b/util/session.lua @@ -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 @@ local function set_send(session) 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 --git a/util/set.lua b/util/set.lua index 69dfef5d..6c860c3f 100644 --- a/util/set.lua +++ b/util/set.lua @@ -186,7 +186,7 @@ function set_mt.__tostring(set) for item in pairs(items) do s[#s+1] = tostring(item); end - return t_concat(s, ", "); + return "{"..t_concat(s, ", ").."}"; end return { diff --git a/util/smqueue.lua b/util/smqueue.lua index 6d8348d4..0514a979 100644 --- a/util/smqueue.lua +++ b/util/smqueue.lua @@ -1,4 +1,4 @@ -local queue = require("util.queue"); +local queue = require("prosody.util.queue"); local lib = { smqueue = {} } diff --git a/util/sql.lua b/util/sql.lua index 9d1c86ca..c897d734 100644 --- a/util/sql.lua +++ b/util/sql.lua @@ -4,9 +4,9 @@ local ipairs = ipairs; 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_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 Integer() return "Integer()" end -local function String() return "String()" end local function Column(definition) return setmetatable(definition, column_mt); @@ -99,6 +97,9 @@ 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:prepquery(sql) if self.params.driver == "MySQL" then @@ -224,6 +225,7 @@ function engine:transaction(...) 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 @@ local function db2uri(params) }; 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 @@ return { is_index = is_index; is_table = is_table; is_query = is_query; - Integer = Integer; - String = String; Column = Column; Table = Table; Index = Index; diff --git a/util/sqlite3.lua b/util/sqlite3.lua new file mode 100644 index 00000000..470eb46d --- /dev/null +++ b/util/sqlite3.lua @@ -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 --git a/util/sslconfig.lua b/util/sslconfig.lua index 6074a1fb..01a8adb5 100644 --- a/util/sslconfig.lua +++ b/util/sslconfig.lua @@ -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 @@ function handlers.options(config, field, new) options[value] = true; end end - config[field] = options; + rawset(config, field, options) end handlers.verifyext = handlers.options; @@ -70,6 +73,30 @@ finalisers.curveslist = finalisers.ciphers; -- 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; + +function finalisers.dhparam(value, config) + if type(value) == "string" then + if value:sub(1, 10) == "-----BEGIN" then + -- literal value + return value; + else + -- assume a filename + return resolve_path(config._basedir, value); + end + end +end + -- protocol = "x" should enable only that protocol -- protocol = "x+" should enable x and later versions @@ -89,37 +116,81 @@ end -- 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 --git a/util/stanza.lua b/util/stanza.lua index 86b88169..2f4e964a 100644 --- a/util/stanza.lua +++ b/util/stanza.lua @@ -21,12 +21,15 @@ local type = type; 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 @@ function stanza_mt:get_child_text(name, xmlns) 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 @@ -266,6 +277,13 @@ function stanza_mt:find(path) local xmlns, name, text; local char = s_sub(path, pos, pos); if char == "@" then + if s_sub(path, pos + 1, pos + 1) == "{" then + return self.attr[s_gsub(s_sub(path, pos+2), "}", "\1", 1)]; + end + local prefix, attr = s_match(path, "^([^:]+):(.*)", pos+1); + if prefix and self.namespaces and self.namespaces[prefix] then + return self.attr[self.namespaces[prefix] .. "\1" .. attr]; + end return self.attr[s_sub(path, pos + 1)]; elseif char == "{" then xmlns, pos = s_match(path, "^([^}]+)}()", pos + 1); @@ -283,25 +301,33 @@ function stanza_mt:find(path) 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 +413,33 @@ function stanza_mt.get_error(stanza) 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 +514,6 @@ local function reply(orig) }); 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 +522,9 @@ local function error_reply(orig, error_type, condition, error_message, error_by) 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 --git a/util/startup.lua b/util/startup.lua index 545b6ae7..caae895d 100644 --- a/util/startup.lua +++ b/util/startup.lua @@ -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 @@ end 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 @@ function startup.set_function_metatable() 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 @@ function startup.init_global_state() 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 @@ function startup.setup_plugin_install_path() 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 @@ end 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 "util.array" - require "util.datetime" - require "util.iterators" - require "util.timer" - require "util.helpers" - - pcall(require, "util.signal") -- Not on Windows + 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 "prosody.util.array" + require "prosody.util.datetime" + require "prosody.util.iterators" + require "prosody.util.timer" + require "prosody.util.helpers" + + pcall(require, "prosody.util.signal") -- Not on Windows -- Commented to protect us from -- the second kind of people @@ -390,43 +390,83 @@ function startup.load_secondary_libraries() 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 @@ function startup.read_version() 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 @@ function startup.log_greeting() 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 @@ function startup.force_console_logging() 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.1"; + 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 @@ end 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 @@ function startup.init_gc() 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 @@ function startup.make_host(hostname) 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,183 @@ function startup.make_dummy_hosts() 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); + os.exit(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; + os.exit(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); + os.exit(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.systemd_notify() + local notify_socket_name = os.getenv("NOTIFY_SOCKET"); + if not notify_socket_name then return end + local have_unix, unix = pcall(require, "socket.unix"); + if not have_unix or type(unix) ~= "table" then + log("error", "LuaSocket without UNIX socket support, can't notify systemd.") + return os.exit(1); + end + log("debug", "Will notify on socket %q", notify_socket_name); + notify_socket_name = notify_socket_name:gsub("^@", "\0"); + local notify_socket = unix.dgram(); + local ok, err = notify_socket:setpeername(notify_socket_name); + if not ok then + log("error", "Could not connect to systemd notification socket %q: %q", notify_socket_name, err); + return os.exit(1); + end + local time = require "prosody.util.time"; + + prosody.notify_socket = notify_socket; + prosody.events.add_handler("server-started", function() + notify_socket:send("READY=1"); + end); + prosody.events.add_handler("reloading-config", function() + notify_socket:send(string.format("RELOADING=1\nMONOTONIC_USEC=%d", math.floor(time.monotonic() * 1000000))); + end); + prosody.events.add_handler("config-reloaded", function() + notify_socket:send("READY=1"); + end); + prosody.events.add_handler("server-stopping", function() + notify_socket:send("STOPPING=1"); + 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"); - prosody.log("info", "Shutdown complete"); + 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 +897,7 @@ function startup.prosody() startup.parse_args(); startup.init_global_state(); startup.read_config(); + startup.check_user(); startup.init_logging(); startup.init_gc(); startup.init_errors(); @@ -704,6 +920,10 @@ function startup.prosody() startup.init_http_client(); startup.init_data_store(); startup.init_global_protection(); + startup.posix_daemonize(); + startup.write_pidfile(); + startup.hook_posix_signals(); + startup.systemd_notify(); startup.prepare_to_start(); startup.notify_started(); end diff --git a/util/statistics.lua b/util/statistics.lua index cb6481c5..a0b5cea9 100644 --- a/util/statistics.lua +++ b/util/statistics.lua @@ -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 --git a/util/statsd.lua b/util/statsd.lua index 581f945a..6c035dfe 100644 --- a/util/statsd.lua +++ b/util/statsd.lua @@ -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 --git a/util/template.lua b/util/template.lua index c11037c5..d14c4c28 100644 --- a/util/template.lua +++ b/util/template.lua @@ -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 --git a/util/termcolours.lua b/util/termcolours.lua index 2c13d0ff..7b6b24a7 100644 --- a/util/termcolours.lua +++ b/util/termcolours.lua @@ -21,7 +21,7 @@ local pairs = pairs; 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 --git a/util/throttle.lua b/util/throttle.lua index d2036e9e..c07bcf68 100644 --- a/util/throttle.lua +++ b/util/throttle.lua @@ -1,5 +1,5 @@ -local gettime = require "util.time".now +local gettime = require "prosody.util.time".now local setmetatable = setmetatable; local _ENV = nil; diff --git a/util/timer.lua b/util/timer.lua index 84da02cf..532bf112 100644 --- a/util/timer.lua +++ b/util/timer.lua @@ -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 --git a/util/uuid.lua b/util/uuid.lua index 54ea99b4..a70750bb 100644 --- a/util/uuid.lua +++ b/util/uuid.lua @@ -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 @@ local function generate() 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 --git a/util/vcard.lua b/util/vcard.lua deleted file mode 100644 index e311f73f..00000000 --- a/util/vcard.lua +++ /dev/null @@ -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 --git a/util/watchdog.lua b/util/watchdog.lua index 516e60e4..70df2530 100644 --- a/util/watchdog.lua +++ b/util/watchdog.lua @@ -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_methods = {}; 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 --git a/util/x509.lua b/util/x509.lua index 76b50076..9ecb5b60 100644 --- a/util/x509.lua +++ b/util/x509.lua @@ -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 - -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"; +-- [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 "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 --git a/util/xml.lua b/util/xml.lua index 2bf1ff4e..e23916be 100644 --- a/util/xml.lua +++ b/util/xml.lua @@ -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 --git a/util/xmppstream.lua b/util/xmppstream.lua index be113396..7f998312 100644 --- a/util/xmppstream.lua +++ b/util/xmppstream.lua @@ -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 --git a/util/xpcall.lua b/util/xpcall.lua index d2fc5011..852df03c 100644 --- a/util/xpcall.lua +++ b/util/xpcall.lua @@ -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 --git a/util/xtemplate.lua b/util/xtemplate.lua index 88baf1f7..446d7d1f 100644 --- a/util/xtemplate.lua +++ b/util/xtemplate.lua @@ -3,13 +3,21 @@ local s_match = string.match; 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 @@ -32,7 +40,7 @@ local function render(template, root, escape, filters) if args then args = s_sub(args, 2, -2); end if func == "each" and tmpl then - if not st.is_stanza(value) then return "" end + if not st.is_stanza(value) then return pre_blank .. post_blank end if not args then value, args = root, path; end local ns, name = s_match(args, "^(%b{})(.*)$"); if ns then @@ -62,11 +70,7 @@ local function render(template, root, escape, filters) 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 + value, is_escaped = f(value, args, tmpl); else error("No such filter function: " .. func); end @@ -75,12 +79,12 @@ local function render(template, root, escape, filters) 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 |